GL
Backend 18 de junio de 2025 20 min de lectura Por Gustavo Leyva

Patrones de diseño en Node.js

Patrones de diseño empresariales para aplicaciones Node.js escalables, incluyendo arquitectura limpia, CQRS, y patrones de microservicios.

#Node.js #Patrones de Diseño #Arquitectura #Backend #Microservicios
P

Introducción

Los patrones de diseño son soluciones probadas a problemas comunes en el desarrollo de software. En Node.js, ciertos patrones son especialmente útiles debido a su naturaleza asíncrona y orientada a eventos. Empresas como LinkedIn, Netflix, PayPal y Uber utilizan estos patrones para construir aplicaciones escalables que manejan millones de usuarios.

1. Patrón Singleton

Garantiza que una clase tenga solo una instancia y proporciona un punto de acceso global a ella. Ideal para conexiones a base de datos, configuraciones y caches.

class DatabaseConnection {
  constructor() {
    if (DatabaseConnection.instance) {
      return DatabaseConnection.instance;
    }
    
    this.connection = null;
    DatabaseConnection.instance = this;
  }
  
  async connect() {
    if (!this.connection) {
      this.connection = await mongoose.connect(process.env.MONGODB_URI, {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        poolSize: 10
      });
      console.log('Database connected');
    }
    return this.connection;
  }
  
  getConnection() {
    if (!this.connection) {
      throw new Error('Database not connected');
    }
    return this.connection;
  }
}

// Siempre devuelve la misma instancia
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();
console.log(db1 === db2); // true

// Exportar instancia única
module.exports = new DatabaseConnection();

2. Patrón Factory

Proporciona una interfaz para crear objetos sin especificar sus clases exactas. Usado por frameworks como NestJS.

// Factory para diferentes tipos de notificaciones
class NotificationFactory {
  createNotification(type, config) {
    switch(type) {
      case 'email':
        return new EmailNotification(config);
      case 'sms':
        return new SMSNotification(config);
      case 'push':
        return new PushNotification(config);
      case 'slack':
        return new SlackNotification(config);
      default:
        throw new Error(`Tipo de notificación no válido: ${type}`);
    }
  }
}

class EmailNotification {
  constructor(config) {
    this.transporter = nodemailer.createTransport(config);
  }
  
  async send(to, subject, body) {
    await this.transporter.sendMail({ to, subject, html: body });
  }
}

class SMSNotification {
  constructor(config) {
    this.client = twilio(config.accountSid, config.authToken);
  }
  
  async send(to, message) {
    await this.client.messages.create({
      body: message,
      to,
      from: this.config.from
    });
  }
}

// Uso
const factory = new NotificationFactory();
const emailNotifier = factory.createNotification('email', emailConfig);
await emailNotifier.send('user@example.com', 'Welcome', 'Hello!');

3. Patrón Observer (EventEmitter)

Node.js tiene soporte nativo para este patrón a través de EventEmitter. Fundamental para arquitecturas orientadas a eventos.

const EventEmitter = require('events');

class OrderProcessor extends EventEmitter {
  async processOrder(order) {
    console.log('Procesando pedido...');
    
    try {
      // Procesar pago
      await this.processPayment(order);
      this.emit('paymentProcessed', order);
      
      // Actualizar inventario
      await this.updateInventory(order);
      this.emit('inventoryUpdated', order);
      
      // Pedido completado
      this.emit('orderCompleted', order);
      
    } catch (error) {
      this.emit('orderFailed', order, error);
    }
  }
  
  async processPayment(order) {
    // Lógica de pago
  }
  
  async updateInventory(order) {
    // Lógica de inventario
  }
}

const processor = new OrderProcessor();

// Suscribirse a eventos
processor.on('paymentProcessed', (order) => {
  console.log(`Pago procesado para pedido ${order.id}`);
  // Enviar confirmación por email
});

processor.on('inventoryUpdated', (order) => {
  console.log(`Inventario actualizado para pedido ${order.id}`);
});

processor.on('orderCompleted', (order) => {
  console.log(`Pedido ${order.id} completado`);
  // Notificar al usuario
});

processor.on('orderFailed', (order, error) => {
  console.error(`Error en pedido ${order.id}:`, error);
  // Rollback y notificación
});

await processor.processOrder({ id: 123, items: [] });

4. Patrón Middleware (Chain of Responsibility)

Fundamental en Express.js y otras frameworks de Node.js. Permite procesar requests a través de una cadena de handlers.

// Middleware de autenticación
const authMiddleware = async (req, res, next) => {
  try {
    const token = req.headers.authorization?.split(' ')[1];
    if (!token) {
      return res.status(401).json({ error: 'No autorizado' });
    }
    
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = await User.findById(decoded.id);
    next();
  } catch (error) {
    res.status(401).json({ error: 'Token inválido' });
  }
};

// Middleware de logging
const logMiddleware = (req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} - ${res.statusCode} - ${duration}ms`);
  });
  
  next();
};

// Middleware de rate limiting
const rateLimitMiddleware = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: 'Demasiadas peticiones'
});

// Middleware de validación
const validateMiddleware = (schema) => {
  return (req, res, next) => {
    const { error } = schema.validate(req.body);
    if (error) {
      return res.status(400).json({ error: error.details[0].message });
    }
    next();
  };
};

// Aplicar middlewares
app.use(logMiddleware);
app.use(rateLimitMiddleware);
app.post('/api/users', validateMiddleware(userSchema), authMiddleware, createUser);

5. Patrón Repository

Abstrae la capa de acceso a datos, facilitando testing y cambios de base de datos. Usado por empresas para mantener código limpio y testeable.

// Interface del repositorio
class UserRepository {
  constructor(database) {
    this.db = database;
  }
  
  async findById(id) {
    return await this.db.users.findOne({ _id: id });
  }
  
  async findByEmail(email) {
    return await this.db.users.findOne({ email });
  }
  
  async findAll(filters = {}, options = {}) {
    const { page = 1, limit = 10, sort = { createdAt: -1 } } = options;
    const skip = (page - 1) * limit;
    
    return await this.db.users
      .find(filters)
      .sort(sort)
      .skip(skip)
      .limit(limit);
  }
  
  async create(userData) {
    const user = new this.db.users(userData);
    return await user.save();
  }
  
  async update(id, userData) {
    return await this.db.users.findByIdAndUpdate(
      id,
      { $set: userData },
      { new: true, runValidators: true }
    );
  }
  
  async delete(id) {
    return await this.db.users.findByIdAndDelete(id);
  }
  
  async count(filters = {}) {
    return await this.db.users.countDocuments(filters);
  }
}

// Uso en servicio
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }
  
  async getUser(id) {
    const user = await this.userRepository.findById(id);
    if (!user) {
      throw new Error('Usuario no encontrado');
    }
    return user;
  }
  
  async createUser(userData) {
    const existingUser = await this.userRepository.findByEmail(userData.email);
    if (existingUser) {
      throw new Error('Email ya registrado');
    }
    
    return await this.userRepository.create(userData);
  }
}

// Dependency Injection
const userRepo = new UserRepository(database);
const userService = new UserService(userRepo);

6. Patrón Dependency Injection

Facilita testing y desacoplamiento. Usado por frameworks como NestJS y InversifyJS.

// Container de dependencias
class Container {
  constructor() {
    this.services = new Map();
  }
  
  register(name, definition, dependencies = []) {
    this.services.set(name, { definition, dependencies });
  }
  
  get(name) {
    const service = this.services.get(name);
    if (!service) {
      throw new Error(`Servicio ${name} no encontrado`);
    }
    
    const { definition, dependencies } = service;
    const resolvedDependencies = dependencies.map(dep => this.get(dep));
    
    return new definition(...resolvedDependencies);
  }
}

// Registro de servicios
const container = new Container();

container.register('database', DatabaseConnection);
container.register('userRepository', UserRepository, ['database']);
container.register('userService', UserService, ['userRepository']);
container.register('authService', AuthService, ['userService']);

// Uso
const authService = container.get('authService');

7. Patrón CQRS (Command Query Responsibility Segregation)

Separa operaciones de lectura y escritura. Usado por aplicaciones de alta escala como LinkedIn.

// Commands (escritura)
class CreateUserCommand {
  constructor(userData) {
    this.userData = userData;
  }
}

class UpdateUserCommand {
  constructor(userId, updates) {
    this.userId = userId;
    this.updates = updates;
  }
}

// Command Handlers
class CreateUserHandler {
  constructor(userRepository, eventBus) {
    this.userRepository = userRepository;
    this.eventBus = eventBus;
  }
  
  async handle(command) {
    const user = await this.userRepository.create(command.userData);
    
    // Emitir evento
    this.eventBus.emit('UserCreated', {
      userId: user.id,
      email: user.email,
      timestamp: new Date()
    });
    
    return user;
  }
}

// Queries (lectura)
class GetUserQuery {
  constructor(userId) {
    this.userId = userId;
  }
}

class GetUsersQuery {
  constructor(filters, pagination) {
    this.filters = filters;
    this.pagination = pagination;
  }
}

// Query Handlers
class GetUserHandler {
  constructor(userReadModel) {
    this.userReadModel = userReadModel;
  }
  
  async handle(query) {
    return await this.userReadModel.findById(query.userId);
  }
}

// Command Bus
class CommandBus {
  constructor() {
    this.handlers = new Map();
  }
  
  register(commandType, handler) {
    this.handlers.set(commandType, handler);
  }
  
  async execute(command) {
    const handler = this.handlers.get(command.constructor);
    if (!handler) {
      throw new Error(`No handler for ${command.constructor.name}`);
    }
    return await handler.handle(command);
  }
}

// Uso
const commandBus = new CommandBus();
commandBus.register(CreateUserCommand, new CreateUserHandler(userRepo, eventBus));

const command = new CreateUserCommand({ email: 'user@example.com', name: 'John' });
const user = await commandBus.execute(command);

8. Patrón Circuit Breaker

Previene fallos en cascada en arquitecturas de microservicios. Usado por Netflix en su stack.

class CircuitBreaker {
  constructor(request, options = {}) {
    this.request = request;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failureCount = 0;
    this.successCount = 0;
    this.nextAttempt = Date.now();
    
    this.threshold = options.threshold || 5;
    this.timeout = options.timeout || 60000;
    this.resetTimeout = options.resetTimeout || 30000;
  }
  
  async call(...args) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF_OPEN';
    }
    
    try {
      const response = await this.request(...args);
      this.onSuccess();
      return response;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    
    if (this.state === 'HALF_OPEN') {
      this.state = 'CLOSED';
      this.successCount = 0;
    }
  }
  
  onFailure() {
    this.failureCount++;
    
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
      console.log('Circuit breaker opened');
    }
  }
}

// Uso con servicio externo
const paymentServiceBreaker = new CircuitBreaker(
  async (orderId, amount) => {
    return await axios.post('https://payment-service.com/charge', {
      orderId,
      amount
    });
  },
  { threshold: 5, resetTimeout: 30000 }
);

try {
  const result = await paymentServiceBreaker.call(orderId, amount);
} catch (error) {
  // Fallback: guardar en cola para retry
  await queuePayment(orderId, amount);
}

9. Patrón Strategy

Permite cambiar algoritmos dinámicamente. Útil para diferentes estrategias de pricing, shipping, etc.

// Estrategias de pricing
class RegularPricing {
  calculate(basePrice, quantity) {
    return basePrice * quantity;
  }
}

class BulkPricing {
  calculate(basePrice, quantity) {
    if (quantity >= 100) return basePrice * quantity * 0.8;
    if (quantity >= 50) return basePrice * quantity * 0.9;
    return basePrice * quantity;
  }
}

class VIPPricing {
  calculate(basePrice, quantity) {
    return basePrice * quantity * 0.85; // 15% descuento
  }
}

// Context
class PriceCalculator {
  constructor(strategy) {
    this.strategy = strategy;
  }
  
  setStrategy(strategy) {
    this.strategy = strategy;
  }
  
  calculate(basePrice, quantity) {
    return this.strategy.calculate(basePrice, quantity);
  }
}

// Uso
const calculator = new PriceCalculator(new RegularPricing());
let price = calculator.calculate(10, 5); // 50

// Cambiar estrategia para cliente VIP
calculator.setStrategy(new VIPPricing());
price = calculator.calculate(10, 5); // 42.5

Conclusión

Los patrones de diseño son fundamentales para construir aplicaciones Node.js escalables y mantenibles. Empresas como LinkedIn, Netflix, PayPal y Uber utilizan estos patrones para:

  • Singleton: Gestión eficiente de recursos compartidos
  • Factory: Creación flexible de objetos
  • Observer: Arquitecturas orientadas a eventos
  • Repository: Abstracción de datos y facilidad de testing
  • Dependency Injection: Desacoplamiento y testabilidad
  • CQRS: Escalabilidad en operaciones de lectura/escritura
  • Circuit Breaker: Resiliencia en microservicios
  • Strategy: Flexibilidad en lógica de negocio

Dominar estos patrones te permitirá diseñar sistemas robustos que escalan a millones de usuarios.

GL

Gustavo Leyva

Desarrollador de Software

← Volver al blog