Skip to main content

Chapter 8: Microservices

Microservices architecture enables you to build scalable, distributed systems. NestJS provides first-class support for microservices, message brokers, and event-driven design. This chapter covers TCP, Redis, RabbitMQ, message patterns, distributed tracing, and real-world deployment tips. We’ll walk through practical examples and explain how to design robust microservices.

8.1 What is a Microservice?

A microservice is a small, independent process that communicates with others over a network. Each service is responsible for a specific business capability and can be deployed, scaled, and updated independently.

Microservices vs Monolith

Monolithic Architecture:
  • Single codebase
  • Deploy as one unit
  • Scale entire application
  • Shared database
Microservices Architecture:
  • Multiple independent services
  • Deploy services independently
  • Scale individual services
  • Each service has its own database
Analogy:
Imagine a team of specialists—each microservice is an expert at one thing, and they communicate via messages. Like a restaurant where the kitchen, waitstaff, and management are separate but coordinated teams.

Benefits

Scalability:
  • Scale only what you need
  • Independent scaling per service
  • Optimize resources
Fault Isolation:
  • One service can fail without crashing the whole system
  • Isolated failures
  • Better resilience
Technology Diversity:
  • Use the best tool for each job
  • Different languages/frameworks per service
  • Technology flexibility
Independent Deployment:
  • Deploy and update services separately
  • Faster release cycles
  • Reduced risk
Team Autonomy:
  • Teams can work independently
  • Clear ownership
  • Faster development

Challenges

  • Complexity: More moving parts
  • Network Latency: Service-to-service communication
  • Data Consistency: Distributed transactions
  • Testing: More complex test scenarios
  • Monitoring: Need distributed tracing

8.2 Microservices in NestJS

NestJS supports multiple transport layers for microservices, each with different characteristics.

Supported Transports

TCP:
  • Default, simple and fast
  • Direct service-to-service communication
  • Low latency
  • Good for internal services
Redis:
  • Pub/sub messaging
  • Caching support
  • Simple setup
  • Good for event-driven systems
NATS:
  • Lightweight messaging
  • High performance
  • Simple protocol
  • Good for cloud-native apps
RabbitMQ:
  • Robust message broker
  • Advanced routing
  • Reliable delivery
  • Good for complex workflows
MQTT:
  • IoT messaging
  • Lightweight protocol
  • Good for IoT applications
gRPC:
  • High-performance RPC
  • Type-safe
  • HTTP/2 based
  • Good for inter-service communication
Kafka:
  • Distributed streaming
  • High throughput
  • Event sourcing support
  • Good for big data
Diagram: Microservice Communication
Service A → [Transport Layer] → Service B
    ↓              ↓                ↓
  HTTP         Message          Database
  API          Broker           Query

8.3 Creating a TCP Microservice

TCP is the simplest transport for microservices. Let’s create a basic TCP microservice.

Installation

npm install @nestjs/microservices

Microservice Bootstrap

// main.ts (microservice)
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.TCP,
      options: {
        host: 'localhost',
        port: 3001,
      },
    },
  );

  await app.listen();
  console.log('Microservice is listening on port 3001');
}
bootstrap();

Message Pattern Handler

// app.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';

@Controller()
export class AppController {
  @MessagePattern({ cmd: 'sum' })
  accumulate(@Payload() data: number[]): number {
    return data.reduce((a, b) => a + b, 0);
  }

  @MessagePattern({ cmd: 'get_user' })
  getUser(@Payload() data: { id: number }) {
    // Return user data
    return { id: data.id, name: 'John Doe' };
  }
}

Client Service

// app.service.ts
import { Injectable, Inject, OnModuleInit } from '@nestjs/common';
import { ClientProxy, ClientProxyFactory, Transport } from '@nestjs/microservices';

@Injectable()
export class AppService implements OnModuleInit {
  private client: ClientProxy;

  constructor() {
    this.client = ClientProxyFactory.create({
      transport: Transport.TCP,
      options: {
        host: 'localhost',
        port: 3001,
      },
    });
  }

  async onModuleInit() {
    await this.client.connect();
  }

  async sum(data: number[]) {
    return this.client.send({ cmd: 'sum' }, data).toPromise();
  }

  async getUser(id: number) {
    return this.client.send({ cmd: 'get_user' }, { id }).toPromise();
  }
}

Hybrid Application

Run both HTTP and microservice in the same app:
// main.ts
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  // HTTP application
  const app = await NestFactory.create(AppModule);

  // Microservice
  app.connectMicroservice<MicroserviceOptions>({
    transport: Transport.TCP,
    options: {
      host: 'localhost',
      port: 3001,
    },
  });

  await app.startAllMicroservices();
  await app.listen(3000);
  console.log('HTTP server on port 3000, Microservice on port 3001');
}
bootstrap();

8.4 Message Patterns

Message patterns define how microservices communicate. Use consistent patterns for maintainability.

Request-Response Pattern

// Handler
@MessagePattern({ cmd: 'get_user' })
getUser(@Payload() data: { id: number }) {
  return this.usersService.findOne(data.id);
}

// Client
const user = await this.client.send({ cmd: 'get_user' }, { id: 1 }).toPromise();

Event Pattern (Fire and Forget)

// Handler
@EventPattern('user_created')
handleUserCreated(@Payload() data: { id: number; email: string }) {
  console.log('User created:', data);
  // Send welcome email, etc.
}

// Client
this.client.emit('user_created', { id: 1, email: '[email protected]' });

Multiple Patterns

@Controller()
export class UsersController {
  @MessagePattern({ cmd: 'create_user' })
  async createUser(@Payload() data: CreateUserDto) {
    return this.usersService.create(data);
  }

  @MessagePattern({ cmd: 'get_user' })
  async getUser(@Payload() data: { id: number }) {
    return this.usersService.findOne(data.id);
  }

  @EventPattern('user_updated')
  async handleUserUpdated(@Payload() data: { id: number }) {
    // Handle event
  }
}
Tip: Use clear, consistent message patterns for maintainability. Document your message contracts.

8.5 Using RabbitMQ

RabbitMQ is a popular message broker for event-driven systems. It enables publish/subscribe and queue-based communication.

Installation

npm install @nestjs/microservices amqplib amqp-connection-manager
npm install --save-dev @types/amqplib

RabbitMQ Microservice

// main.ts
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.RMQ,
      options: {
        urls: [process.env.RABBITMQ_URL || 'amqp://localhost:5672'],
        queue: 'orders_queue',
        queueOptions: {
          durable: true,
        },
      },
    },
  );

  await app.listen();
  console.log('RabbitMQ microservice is listening');
}
bootstrap();

RabbitMQ Client

// app.service.ts
import { Injectable } from '@nestjs/common';
import { ClientProxy, ClientProxyFactory, Transport } from '@nestjs/microservices';

@Injectable()
export class AppService {
  private client: ClientProxy;

  constructor() {
    this.client = ClientProxyFactory.create({
      transport: Transport.RMQ,
      options: {
        urls: [process.env.RABBITMQ_URL || 'amqp://localhost:5672'],
        queue: 'orders_queue',
        queueOptions: {
          durable: true,
        },
      },
    });
  }

  async createOrder(orderData: any) {
    return this.client.send({ cmd: 'create_order' }, orderData).toPromise();
  }

  async publishOrderCreated(order: any) {
    this.client.emit('order_created', order);
  }
}
Diagram: Event-Driven Flow
Producer Service

Publish Event → [RabbitMQ Queue]

Consumer Service(s) ← Subscribe to Queue

Process Event

8.6 Using Redis

Redis provides pub/sub messaging and can be used as a transport layer.

Redis Microservice

// main.ts
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.REDIS,
      options: {
        host: 'localhost',
        port: 6379,
      },
    },
  );

  await app.listen();
}
bootstrap();

Redis Client

this.client = ClientProxyFactory.create({
  transport: Transport.REDIS,
  options: {
    host: 'localhost',
    port: 6379,
  },
});

8.7 Event-Driven Design

Event-driven architecture decouples services and enables scalability.

Principles

Decoupling:
  • Services don’t need to know about each other
  • Communicate via events
  • Loose coupling
Scalability:
  • Publish/subscribe patterns
  • Multiple consumers
  • Horizontal scaling
Reliability:
  • Message persistence
  • Retries and dead-letter queues
  • Event replay

Event Sourcing Integration

@EventPattern('order_created')
async handleOrderCreated(@Payload() event: OrderCreatedEvent) {
  // Store event
  await this.eventStore.append('order_created', event);
  
  // Update read model
  await this.updateReadModel(event);
  
  // Publish to other services
  this.client.emit('order_created_notification', event);
}

Correlation IDs

Track requests across services:
@MessagePattern({ cmd: 'process_order' })
async processOrder(@Payload() data: any, @Ctx() context: RmqContext) {
  const correlationId = context.getMessage().properties.correlationId;
  
  // Use correlation ID for tracing
  this.logger.log(`Processing order with correlation ID: ${correlationId}`);
  
  return this.ordersService.process(data);
}

8.8 Distributed Tracing & Monitoring

Monitor and trace requests across microservices.

OpenTelemetry Integration

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { trace, context } from '@opentelemetry/api';

@Injectable()
export class TracingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const tracer = trace.getTracer('nestjs');
    const span = tracer.startSpan('http_request');
    
    return next.handle().pipe(
      tap(() => span.end()),
      catchError((err) => {
        span.recordException(err);
        span.end();
        throw err;
      }),
    );
  }
}

Logging with Correlation IDs

@Injectable()
export class LoggerService {
  log(message: string, correlationId?: string) {
    console.log(JSON.stringify({
      message,
      correlationId,
      timestamp: new Date().toISOString(),
    }));
  }
}

8.9 Error Handling

Handle errors gracefully in microservices.

Error Handling Pattern

@MessagePattern({ cmd: 'process_order' })
async processOrder(@Payload() data: any) {
  try {
    return await this.ordersService.process(data);
  } catch (error) {
    // Log error
    this.logger.error('Failed to process order', error);
    
    // Emit error event
    this.client.emit('order_processing_failed', {
      orderId: data.id,
      error: error.message,
    });
    
    throw error;
  }
}

Retry Logic

import { retry, catchError } from 'rxjs/operators';

this.client.send({ cmd: 'process_order' }, data).pipe(
  retry(3),
  catchError((error) => {
    // Handle error after retries
    return throwError(() => error);
  }),
).toPromise();

8.10 Best Practices

Following best practices ensures your microservices are robust and maintainable.

Service Design

  • Keep services small and focused (single responsibility)
  • Define clear service boundaries
  • Use domain-driven design
  • Document service contracts

Communication

  • Use message brokers for async communication
  • Use request-response for synchronous needs
  • Implement circuit breakers
  • Handle timeouts gracefully

Data Management

  • Each service owns its data
  • Avoid shared databases
  • Use event sourcing for consistency
  • Implement saga pattern for distributed transactions

Security

  • Secure communication between services (TLS)
  • Authenticate service-to-service calls
  • Use API keys or mTLS
  • Validate all inputs

Deployment

  • Containerize services (Docker)
  • Use orchestration (Kubernetes)
  • Implement health checks
  • Enable auto-scaling

Monitoring

  • Log all important events
  • Use distributed tracing
  • Monitor latency and errors
  • Set up alerts

Documentation

  • Document message patterns
  • Document service APIs
  • Maintain service registry
  • Keep architecture diagrams updated

8.11 Summary

You’ve learned how to build and scale microservices with NestJS: Key Concepts:
  • Microservices: Independent, scalable services
  • Transport Layers: TCP, Redis, RabbitMQ, NATS, gRPC
  • Message Patterns: Request-response and event patterns
  • Event-Driven: Decoupled, scalable architecture
  • Distributed Tracing: Monitor requests across services
Best Practices:
  • Keep services small and focused
  • Use message brokers for communication
  • Handle errors gracefully
  • Secure service communication
  • Monitor and trace requests
  • Document service contracts
Next Chapter: Learn about deployment, production optimization, Docker, Kubernetes, and monitoring strategies.