Microservices architecture has become the backbone of modern, scalable, and resilient applications. In this article, weโll explore powerful design patterns that make microservices shine, specifically in the context of NestJS, a progressive Node.js framework.
1. ๐ก๏ธ Gateway Pattern
The Gateway Pattern acts as the single entry point for all microservice calls. It routes requests to the appropriate service, handles authentication, and logging, and can even aggregate responses.
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { GatewayController } from './gateway.controller';
@Module({
imports: [
ClientsModule.register([
{ name: 'USER_SERVICE', transport: Transport.TCP },
{ name: 'ORDER_SERVICE', transport: Transport.TCP },
]),
],
controllers: [GatewayController],
})
export class GatewayModule {}
import { Controller, Get } from '@nestjs/common';
import { ClientProxy, ClientProxyFactory, Transport } from '@nestjs/microservices';
@Controller('gateway')
export class GatewayController {
private userServiceClient: ClientProxy;
private orderServiceClient: ClientProxy;
constructor() {
this.userServiceClient = ClientProxyFactory.create({ transport: Transport.TCP, options: { port: 3001 } });
this.orderServiceClient = ClientProxyFactory.create({ transport: Transport.TCP, options: { port: 3002 } });
}
@Get('user')
getUser() {
return this.userServiceClient.send({ cmd: 'get-user' }, {});
}
@Get('order')
getOrder() {
return this.orderServiceClient.send({ cmd: 'get-order' }, {});
}
}
2. ๐ก Service Registry Pattern
The Service Registry Pattern allows microservices to discover each other without hardcoding their locations. Itโs essential in dynamic environments where services may change IPs or ports.
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Consul } from 'consul';
@Injectable()
export class ServiceRegistry implements OnModuleInit {
private consul: Consul;
constructor() {
this.consul = new Consul();
}
onModuleInit() {
this.consul.agent.service.register({
name: 'user-service',
address: '127.0.0.1',
port: 3001,
});
}
}
3. โก Circuit Breaker Pattern
The Circuit Breaker Pattern prevents cascading failures in microservices by breaking the circuit and returning a fallback response when a service fails or is slow to respond.
import { Injectable, HttpService } from '@nestjs/common';
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
@Injectable()
export class CircuitBreakerService {
constructor(private httpService: HttpService) {}
getUserData() {
return this.httpService.get('http://user-service/user')
.pipe(
catchError(err => {
console.log('Service unavailable, returning fallback data');
return of({ id: 'fallback', name: 'Fallback User' });
})
);
}
}
4. ๐ SAGA Pattern
The SAGA Pattern manages complex transactions across multiple services by breaking them into smaller steps. Each step in the SAGA can either be completed successfully or trigger compensating transactions to undo the previous steps if something goes wrong.
import { Injectable } from '@nestjs/common';
import { EventPattern } from '@nestjs/microservices';
@Injectable()
export class SagaService {
@EventPattern('order-created')
async handleOrderCreated(data: Record<string, unknown>) {
// Reserve inventory
// If inventory reservation fails, trigger a compensating transaction
}
@EventPattern('payment-processed')
async handlePaymentProcessed(data: Record<string, unknown>) {
// Confirm order
// If payment fails, trigger a compensating transaction to release inventory
}
}
5. ๐ง CQRS (Command Query Responsibility Segregation)
CQRS separates read and write operations, improving performance by optimizing each operation type independently. Itโs particularly useful in systems with complex querying requirements.
import { QueryHandler, IQueryHandler } from '@nestjs/cqrs';
export class GetUserQuery {
constructor(public readonly userId: string) {}
}
@QueryHandler(GetUserQuery)
export class GetUserHandler implements IQueryHandler<GetUserQuery> {
async execute(query: GetUserQuery) {
// Handle the query, e.g., return user data
return { id: query.userId, name: 'John Doe' };
}
}
6. ๐งฑ Bulkhead Pattern
The Bulkhead Pattern isolates components within a service to prevent failures from spreading, ensuring that one failing service doesnโt bring down others.
import { Injectable } from '@nestjs/common';
import { Queue } from 'bull';
@Injectable()
export class BulkheadService {
private readonly taskQueue: Queue;
constructor() {
this.taskQueue = new Queue('tasks');
}
async handleTask(taskData: any) {
await this.taskQueue.add(taskData);
// Process task without affecting other components
}
}
7. ๐ Sidecar Pattern
The Sidecar Pattern adds extra functionalities (like monitoring, logging, or proxying) to a service without altering the core service logic.
import { Injectable } from '@nestjs/common';
import { createProxyMiddleware } from 'http-proxy-middleware';
@Injectable()
export class SidecarService {
configure(app: any) {
app.use('/user', createProxyMiddleware({ target: 'http://localhost:3001', changeOrigin: true }));
}
}
8. ๐ API Composition Pattern
API Composition orchestrates multiple microservices into a single API response, which is useful when building APIs that aggregate data from different sources.
import { Controller, Get } from '@nestjs/common';
import { HttpService } from '@nestjs/common';
@Controller('orders')
export class OrdersController {
constructor(private httpService: HttpService) {}
@Get()
async getOrders() {
const user = await this.httpService.get('http://user-service/user').toPromise();
const order = await this.httpService.get('http://order-service/order').toPromise();
return { ...user.data, ...order.data };
}
}
9. โ๏ธ Event-Driven Architecture
In Event-Driven Architecture, services react to events asynchronously, making the system more responsive and decoupled.
import { Controller } from '@nestjs/common';
import { EventPattern } from '@nestjs/microservices';
@Controller()
export class EventController {
@EventPattern('user_created')
handleUserCreated(data: Record<string, unknown>) {
console.log('User created event received:', data);
// React to the event
}
}
10. ๐ Database per Service
Each microservice owns its data, stored in its database, ensuring data isolation and independence. This pattern allows microservices to evolve independently.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UserService {
constructor(@InjectRepository(User) private userRepository: Repository<User>) {}
findAll() {
return this.userRepository.find();
}
}
11. ๐ Retry Pattern
The Retry Pattern handles transient failures by retrying failed operations, often with an exponential backoff strategy.
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/common';
import { catchError, retryWhen, delay, take } from 'rxjs/operators';
import { of } from 'rxjs';
@Injectable()
export class RetryService {
constructor(private httpService: HttpService) {}
fetchData() {
return this.httpService.get('http://unreliable-service/data')
.pipe(
retryWhen(errors => errors.pipe(delay(1000), take(3))),
catchError(err => {
console.log('Failed after retries');
return of({ fallback: true });
})
);
}
}
12. ๐ Configuration Externalization
Configuration Externalization centralizes configuration management, making it easier to change configuration without redeploying services.
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
],
})
export class AppModule {}
Conclusion
Microservices architecture, combined with design patterns like those mentioned above, empowers applications to be flexible, resilient, and scalable. NestJS, with its modular approach and powerful abstractions, is an excellent framework for implementing these patterns, ensuring that your application can handle the complexities of modern distributed systems.
Leave a Reply