La arquitectura de microservicios se ha convertido en la columna vertebral de las aplicaciones modernas, escalables y resistentes. En este artículo, exploraremos poderosos patrones de diseño que hacen brillar a los microservicios, específicamente en el contexto de NestJS, un framework progresivo de Node.js.

🛡️ Patrón Gateway

El patrón Gateway actúa como punto de entrada único para todas las llamadas a microservicios. Enruta las solicitudes al servicio adecuado, gestiona la autenticación y el registro, e incluso puede agregar respuestas.

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' }, {});
  }
}

📡 Patrón de registro de servicios

El patrón de registro de servicios permite que los microservicios se descubran entre sí sin codificar su ubicación. Es esencial en entornos dinámicos en los que los servicios pueden cambiar de IP o de puerto.

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,
    });
  }
}

⚡ Patrón del interruptor automático.

El Circuit Breaker Pattern evita fallos en cascada en microservicios rompiendo el circuito y devolviendo una respuesta alternativa cuando un servicio falla o responde con lentitud.

CPU
1 vCPU
MEMORIA
1 GB
ALMACENAMIENTO
10 GB
TRANSFERENCIA
1 TB
PRECIO
$ 4 mes
Para obtener el servidor GRATIS debes de escribir el cupon "LEIFER"
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' });
        })
      );
  }
}

🔄 Patrón SAGA

El patrón SAGA gestiona transacciones complejas entre múltiples servicios dividiéndolas en pasos más pequeños. Cada paso del SAGA puede completarse con éxito o desencadenar transacciones compensatorias para deshacer los pasos anteriores si algo sale mal.

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
  }
}

🔧 CQRS (segregación de responsabilidad de consulta de comandos)

CQRS separa las operaciones de lectura y escritura, mejorando el rendimiento al optimizar cada tipo de operación de forma independiente. Es especialmente útil en sistemas con requisitos de consulta complejos.

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' };
  }
}

🧱 Patrón de mamparo

El patrón Bulkhead aísla los componentes dentro de un servicio para evitar que los fallos se propaguen, asegurando que un servicio que falla no hace caer a los demás.

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
  }
}

🚗 Patrón Sidecar

El patrón Sidecar añade funcionalidades adicionales (como monitorización, registro o proxy) a un servicio sin alterar la lógica central del servicio.

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 }));
  }
}

🔗 Patrón de composición de la API

La composición de API orquesta varios microservicios en una única respuesta de API, lo que resulta útil cuando se crean API que agregan datos de distintas fuentes.

 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 };
  }
}

⚙️ Arquitectura basada en eventos

En la arquitectura dirigida por eventos, los servicios reaccionan a los eventos de forma asíncrona, lo que hace que el sistema sea más receptivo y esté más desacoplado.

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
  }
}

📊 Base de datos por servicio

Cada microservicio es propietario de sus datos, almacenados en su base de datos, lo que garantiza el aislamiento y la independencia de los datos. Este patrón permite que los microservicios evolucionen de forma independiente.

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();
  }
}

🔁 Patrón de reintentos

El patrón de reintento gestiona los fallos transitorios reintentando las operaciones fallidas, a menudo con una estrategia de backoff exponencial.

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 });
        })
      );
  }
}

📂 Externalización de la configuración

La externalización de la configuración centraliza la gestión de la configuración, lo que facilita el cambio de configuración sin necesidad de volver a desplegar los servicios.

import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
  ],
})
export class AppModule {}

Conclusión

La arquitectura de microservicios, combinada con patrones de diseño como los mencionados anteriormente, permite que las aplicaciones sean flexibles, resistentes y escalables. NestJS, con su enfoque modular y poderosas abstracciones, es un excelente framework para implementar estos patrones, asegurando que tu aplicación pueda manejar las complejidades de los sistemas distribuidos modernos.

Fuente