Backend
Backend - Patrones de Diseño
Patrones de diseño fundamentales para desarrollo backend: Singleton, Factory, Repository, Dependency Injection y Circuit Breaker
ConceptosBest Practices
Introducción
Los patrones de diseño son soluciones reutilizables a problemas comunes en el desarrollo de software. Conocerlos te ayudará a escribir código más mantenible y escalable.
Singleton
1 preguntas
1
¿Qué es el patrón Singleton y cuándo usarlo?
Definición: Asegura que una clase tenga solo una instancia y proporciona un punto de acceso global a ella.
Características:
✓Solo una instancia en toda la aplicación
✓Acceso global controlado
✓Inicialización perezosa (lazy initialization)
Implementación en JavaScript/TypeScript:
Clásica:
javascript
class DatabaseConnection {
constructor() {
if (DatabaseConnection.instance) {
return DatabaseConnection.instance;
}
this.connection = this.connect();
DatabaseConnection.instance = this;
return this;
}
connect() {
// Lógica de conexión
return { connected: true };
}
}
// Uso
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();
console.log(db1 === db2); // true - misma instanciaCon módulo ES6:
javascript
// database.js
export default new DatabaseConnection();
// Siempre la misma instancia al importarEn Node.js (módulos son singletons):
javascript
// logger.js
class Logger {
log(message) {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
module.exports = new Logger(); // Singleton por defectoCuándo usar:
✓Recursos compartidos:
• Conexión a base de datos
• Logger
• Cache
• Configuración
✓Control de acceso:
• Pool de conexiones
• Gestor de archivos
Cuándo NO usar:
✗ Testing: Dificulta testing (mocks)
✗ Estado global: Puede causar problemas
✗ Concurrencia: Puede tener problemas en multi-thread
Problemas comunes:
✗ Testing difícil:
javascript
// ❌ Difícil de mockear
const db = DatabaseConnection.getInstance();
// ✅ Mejor: Dependency Injection
class UserService {
constructor(db) {
this.db = db; // Inyectado, fácil de mockear
}
}✗ Estado global:
• Puede causar efectos secundarios
• Dificulta debugging
Alternativas modernas:
✓Dependency Injection: En lugar de singleton global
✓Módulos ES6: Ya son singletons por naturaleza
✓Contenedores DI: NestJS, InversifyJS
Mejores prácticas:
•Usar solo cuando realmente necesitas una instancia única
•Preferir Dependency Injection cuando sea posible
•Documentar por qué es singleton
•Considerar thread-safety si aplica
Factory
1 preguntas
1
¿Qué es el patrón Factory y cuándo usarlo?
Definición: Patrón que proporciona una interfaz para crear objetos sin especificar su clase exacta.
Tipos:
1. Simple Factory:
javascript
class UserFactory {
static create(type) {
switch (type) {
case 'admin':
return new AdminUser();
case 'regular':
return new RegularUser();
default:
throw new Error('Unknown user type');
}
}
}
// Uso
const admin = UserFactory.create('admin');
const user = UserFactory.create('regular');2. Factory Method:
javascript
class DatabaseFactory {
createConnection(type) {
if (type === 'mysql') {
return new MySQLConnection();
} else if (type === 'postgres') {
return new PostgresConnection();
}
}
}
class MySQLFactory extends DatabaseFactory {
createConnection() {
return new MySQLConnection();
}
}3. Abstract Factory:
javascript
// Factory para crear familias de objetos relacionados
class UIFactory {
createButton() {}
createDialog() {}
}
class WindowsFactory extends UIFactory {
createButton() { return new WindowsButton(); }
createDialog() { return new WindowsDialog(); }
}
class MacFactory extends UIFactory {
createButton() { return new MacButton(); }
createDialog() { return new MacDialog(); }
}Cuándo usar:
✓Creación compleja:
• Objetos con lógica de creación compleja
• Múltiples pasos para crear objeto
✓Múltiples tipos:
• Diferentes variantes del mismo tipo
• Selección basada en configuración
✓Desacoplamiento:
• Cliente no conoce clases concretas
• Fácil agregar nuevos tipos
Ejemplo práctico:
javascript
// Factory para crear diferentes tipos de notificaciones
class NotificationFactory {
static create(type, config) {
switch (type) {
case 'email':
return new EmailNotification(config.email);
case 'sms':
return new SMSNotification(config.phone);
case 'push':
return new PushNotification(config.deviceId);
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
}
// Uso
const notification = NotificationFactory.create('email', { email: 'user@example.com' });
notification.send('Hello!');Ventajas:
✓Desacopla creación de uso
✓Fácil agregar nuevos tipos
✓Centraliza lógica de creación
✓Facilita testing (mock factory)
Desventajas:
✗ Puede añadir complejidad
✗ Más código
✗ Puede volverse complejo con muchos tipos
Mejores prácticas:
•Usar cuando la creación es compleja
•Mantener factory simple
•Considerar Builder para objetos muy complejos
Repository
1 preguntas
1
¿Qué es el patrón Repository y cuándo usarlo?
Definición: Capa de abstracción entre la lógica de negocio y la capa de acceso a datos.
Propósito:
✓Abstraer acceso a datos
✓Facilitar testing (mock repository)
✓Cambiar implementación de DB sin afectar lógica
✓Centralizar queries
Estructura básica:
typescript
// Interface
interface UserRepository {
findById(id: string): Promise<User | null>;
findAll(): Promise<User[]>;
create(user: User): Promise<User>;
update(id: string, user: Partial<User>): Promise<User>;
delete(id: string): Promise<void>;
}
// Implementación con SQL
class SQLUserRepository implements UserRepository {
constructor(private db: Database) {}
async findById(id: string): Promise<User | null> {
const row = await this.db.query(
'SELECT * FROM users WHERE id = ?',
[id]
);
return row ? this.toUser(row) : null;
}
async create(user: User): Promise<User> {
const result = await this.db.query(
'INSERT INTO users (name, email) VALUES (?, ?)',
[user.name, user.email]
);
return { ...user, id: result.insertId };
}
// ... otros métodos
}
// Implementación con MongoDB
class MongoUserRepository implements UserRepository {
constructor(private collection: Collection) {}
async findById(id: string): Promise<User | null> {
const doc = await this.collection.findOne({ _id: id });
return doc ? this.toUser(doc) : null;
}
// ... otros métodos
}
// Uso en servicio
class UserService {
constructor(private userRepo: UserRepository) {}
async getUser(id: string) {
return await this.userRepo.findById(id);
}
}Ventajas:
✓Testing:
javascript
// Mock repository para tests
class MockUserRepository implements UserRepository {
async findById(id: string) {
return { id, name: 'Test User' };
}
}
// Test
const service = new UserService(new MockUserRepository());✓Cambio de DB:
• Cambiar de SQL a MongoDB sin cambiar lógica de negocio
• Solo cambiar implementación del repository
✓Separación de concerns:
• Lógica de negocio no conoce detalles de DB
• Queries centralizadas
✓Reutilización:
• Múltiples servicios pueden usar mismo repository
Cuándo usar:
✓Aplicaciones complejas:
• Múltiples fuentes de datos
• Lógica de negocio compleja
✓Testing importante:
• Necesitas mockear acceso a datos
✓Cambios de DB frecuentes:
• Puede cambiar de SQL a NoSQL
✓Clean Architecture:
• Parte de arquitectura limpia
Cuándo NO usar:
✗ Proyectos simples:
• CRUD básico
• Overhead innecesario
✗ Queries muy específicas:
• Puede volverse complejo
Mejores prácticas:
✓Una entidad = Un repository:
• UserRepository para User
• OrderRepository para Order
✓Métodos específicos del dominio:
javascript
// ✅ Específico del dominio
async findActiveUsers(): Promise<User[]> {
return this.db.query('SELECT * FROM users WHERE status = ?', ['active']);
}
// ❌ Genérico (mejor en service)
async findByField(field: string, value: any): Promise<User[]> {
// Demasiado genérico
}✓Retornar entidades de dominio:
• No DTOs de DB directamente
• Mapear a objetos de dominio
✓Manejar errores de DB:
• Capturar y convertir a errores de dominio
Dependency Injection
1 preguntas
1
¿Qué es Dependency Injection y por qué es importante?
Definición: Patrón donde las dependencias se "inyectan" desde fuera en lugar de ser creadas dentro de la clase.
Problema sin DI:
javascript
// ❌ SIN Dependency Injection
class UserService {
constructor() {
this.db = new Database(); // Dependencia hardcodeada
this.logger = new Logger(); // Dependencia hardcodeada
}
async getUser(id) {
this.logger.log(`Getting user ${id}`);
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
// Problemas:
// - Difícil de testear (no puedes mockear DB)
// - Acoplamiento fuerte
// - No puedes cambiar implementaciónSolución con DI:
javascript
// ✅ CON Dependency Injection
class UserService {
constructor(db, logger) {
this.db = db; // Inyectado
this.logger = logger; // Inyectado
}
async getUser(id) {
this.logger.log(`Getting user ${id}`);
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
// Uso
const db = new Database();
const logger = new Logger();
const userService = new UserService(db, logger);
// Testing - fácil de mockear
const mockDb = { query: jest.fn() };
const mockLogger = { log: jest.fn() };
const service = new UserService(mockDb, mockLogger);Tipos de inyección:
1. Constructor Injection (más común):
javascript
class UserService {
constructor(db, logger) {
this.db = db;
this.logger = logger;
}
}2. Property Injection:
javascript
class UserService {
setDatabase(db) {
this.db = db;
}
}3. Method Injection:
javascript
class UserService {
getUser(id, db) {
return db.query('...', [id]);
}
}Ventajas:
✓Testabilidad:
javascript
// Fácil crear mocks
const mockDb = { query: () => Promise.resolve(mockUser) };
const service = new UserService(mockDb, mockLogger);✓Desacoplamiento:
• Clase no depende de implementación concreta
• Depende de abstracción (interfaz)
✓Flexibilidad:
• Cambiar implementación fácilmente
• Configuración externa
✓Reutilización:
• Mismas dependencias en múltiples clases
Ejemplo con interfaces:
typescript
// Interface (abstracción)
interface Database {
query(sql: string, params: any[]): Promise<any>;
}
// Implementación concreta
class MySQLDatabase implements Database {
async query(sql: string, params: any[]) {
// Implementación MySQL
}
}
class PostgresDatabase implements Database {
async query(sql: string, params: any[]) {
// Implementación Postgres
}
}
// Servicio depende de abstracción
class UserService {
constructor(private db: Database) {} // Interface, no clase concreta
async getUser(id: string) {
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
// Inyección
const db = new MySQLDatabase(); // O PostgresDatabase
const service = new UserService(db);Contenedores DI:
NestJS (built-in):
typescript
@Injectable()
export class UserService {
constructor(
@Inject('DATABASE') private db: Database,
private logger: Logger
) {}
}InversifyJS:
typescript
@injectable()
class UserService {
constructor(
@inject('Database') private db: Database
) {}
}Cuándo usar:
✓Aplicaciones complejas:
• Múltiples dependencias
• Testing importante
✓Clean Architecture:
• Parte fundamental
✓Equipos grandes:
• Facilita colaboración
Mejores prácticas:
✓Inyectar dependencias, no crear:
• No usar
new dentro de clases✓Depender de abstracciones:
• Interfaces, no clases concretas
✓Constructor injection preferido:
• Más claro y explícito
✓Un contenedor DI:
• Centralizar creación de objetos
• Gestión de ciclo de vida
Circuit Breaker
1 preguntas
1
¿Qué es el patrón Circuit Breaker y cuándo usarlo?
Definición: Patrón que previene llamadas repetidas a un servicio que está fallando, permitiendo que se recupere.
Analogía: Como un interruptor eléctrico que se "abre" cuando hay demasiada corriente.
Estados del Circuit Breaker:
1. Closed (Cerrado):
•Estado normal
•Requests pasan normalmente
•Monitorea fallos
2. Open (Abierto):
•Servicio está fallando
•Requests son rechazados inmediatamente
•No intenta llamar al servicio
•Timeout antes de intentar de nuevo
3. Half-Open (Semi-abierto):
•Estado de prueba
•Permite algunos requests
•Si tienen éxito → Closed
•Si fallan → Open
Flujo:
[Closed] → (muchos fallos) → [Open] → (timeout) → [Half-Open]
↓
(éxito) → [Closed]
(fallo) → [Open]Implementación básica:
javascript
class CircuitBreaker {
constructor(service, options = {}) {
this.service = service;
this.failureThreshold = options.failureThreshold || 5;
this.timeout = options.timeout || 60000; // 1 minuto
this.resetTimeout = options.resetTimeout || 30000; // 30 segundos
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.failureCount = 0;
this.nextAttempt = Date.now();
}
async call(...args) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
// Intentar half-open
this.state = 'HALF_OPEN';
}
try {
const result = await this.service(...args);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.resetTimeout;
}
}
}
// Uso
const breaker = new CircuitBreaker(async (url) => {
const response = await fetch(url);
if (!response.ok) throw new Error('Service failed');
return response.json();
});
try {
const data = await breaker.call('http://api.example.com/data');
} catch (error) {
// Fallback o error handling
console.log('Service unavailable, using cache');
}Cuándo usar:
✓Llamadas a servicios externos:
• APIs de terceros
• Microservicios
• Bases de datos remotas
✓Prevenir cascading failures:
• Si servicio A cae, no afectar servicio B
• Aislar fallos
✓Mejorar resiliencia:
• Sistema sigue funcionando aunque servicios externos fallen
• Fallback a cache o datos alternativos
Ejemplo práctico:
javascript
// Servicio con Circuit Breaker
class PaymentService {
constructor() {
this.breaker = new CircuitBreaker(
this.callPaymentAPI.bind(this),
{
failureThreshold: 5,
resetTimeout: 60000
}
);
}
async processPayment(amount) {
try {
return await this.breaker.call(amount);
} catch (error) {
// Fallback: guardar en cola para procesar después
await this.queuePayment(amount);
return { status: 'queued', message: 'Payment queued for later processing' };
}
}
async callPaymentAPI(amount) {
const response = await fetch('http://payment-api/process', {
method: 'POST',
body: JSON.stringify({ amount })
});
if (!response.ok) {
throw new Error('Payment API failed');
}
return response.json();
}
}Librerías:
✓opossum (Node.js):
javascript
const CircuitBreaker = require('opossum');
const options = {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000
};
const breaker = new CircuitBreaker(apiCall, options);
breaker.on('open', () => console.log('Circuit opened'));
breaker.on('halfOpen', () => console.log('Circuit half-open'));Ventajas:
✓Previene sobrecarga de servicios caídos
✓Falla rápido (fail-fast)
✓Permite recuperación
✓Mejora resiliencia del sistema
Desventajas:
✗ Añade complejidad
✗ Puede rechazar requests válidos temporalmente
✗ Necesita configuración cuidadosa
Mejores prácticas:
✓Configurar thresholds apropiados:
• No muy bajo (se abre muy rápido)
• No muy alto (permite muchos fallos)
✓Implementar fallback:
• Cache
• Datos por defecto
• Cola para procesar después
✓Monitorear estados:
• Logs cuando cambia estado
• Métricas de circuit breaker
✓Timeouts apropiados:
• Reset timeout basado en tiempo de recuperación esperado