Asegurar una Única Instancia con el Singleton Pattern
Cómo garantizar exactamente una instancia de una clase en una aplicación usando inicialización perezosa, creación thread-safe y singletons basados en registro.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Visión general
Algunos recursos son inherentemente singulares dentro del alcance de una aplicación: un pool de conexiones a base de datos, un gestor de configuración, un framework de logging o un cache en memoria. Crear múltiples instancias de estos recursos desperdicia memoria, causa inconsistencia de estado y puede agotar límites del sistema (ej. demasiadas conexiones a base de datos). El singleton pattern asegura que una clase tenga exactamente una instancia y provee un punto de acceso global a ella.
La implementación ingenua — un campo estático inicializado al cargar la clase — funciona para casos simples pero falla bajo concurrencia y hace el testing difícil. Un test que muta el estado del singleton filtra esa mutación a tests posteriores. Las implementaciones modernas usan inicialización perezosa, inyección de dependencias o registros para balancear rendimiento, thread safety y testeabilidad. El siguiente enfoque cubre la evolución desde singletons básicos hasta producción.
Cuándo usarlo
Usa esta receta cuando:
- Una clase gestiona un recurso que debe ser único dentro de la aplicación (pool de conexiones, cache, config). Consulta Factory Pattern para patrones de creación.
- Múltiples instancias causarían conflictos o agotamiento de recursos. Consulta Connection Pooling para recursos compartidos.
- Necesitas inicialización perezosa para evitar setup costoso durante el arranque
- El singleton es stateless o read-only después de la inicialización (evita estado global mutable). Consulta Locks y Mutexes para acceso thread-safe.
Solución
Singleton Thread-Safe (Java)
public class DatabaseConnectionPool {
private static volatile DatabaseConnectionPool instance;
private static final Object lock = new Object();
private final HikariDataSource dataSource;
private DatabaseConnectionPool() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(System.getenv("DATABASE_URL"));
config.setMaximumPoolSize(10);
this.dataSource = new HikariDataSource(config);
}
public static DatabaseConnectionPool getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new DatabaseConnectionPool();
}
}
}
return instance;
}
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
Singleton a Nivel de Módulo (Python)
from psycopg2 import pool
class DatabaseConnectionPool:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialize()
return cls._instance
def _initialize(self):
self.pool = pool.ThreadedConnectionPool(
minconn=2, maxconn=10,
dsn="postgresql://user:pass@localhost/db"
)
def get_connection(self):
return self.pool.getconn()
def release_connection(self, conn):
self.pool.putconn(conn)
# Los imports del modulo dan la misma instancia en todas partes
from connection_pool import DatabaseConnectionPool
pool = DatabaseConnectionPool()
Singleton Basado en Registro (TypeScript)
class SingletonRegistry {
private static instances: Map<string, unknown> = new Map();
static get<T>(key: string, factory: () => T): T {
if (!SingletonRegistry.instances.has(key)) {
SingletonRegistry.instances.set(key, factory());
}
return SingletonRegistry.instances.get(key) as T;
}
static reset(key: string): void {
SingletonRegistry.instances.delete(key);
}
static clear(): void {
SingletonRegistry.instances.clear();
}
}
const pool = SingletonRegistry.get('db-pool', () => new ConnectionPool());
SingletonRegistry.reset('db-pool'); // para tests
Singleton con DI Container (C# / .NET)
builder.Services.AddSingleton<IDatabaseConnectionPool, DatabaseConnectionPool>();
public class OrderService {
private readonly IDatabaseConnectionPool _pool;
public OrderService(IDatabaseConnectionPool pool) {
_pool = pool;
}
public async Task<Order> GetOrder(int id) {
await using var conn = await _pool.GetConnectionAsync();
// ...
}
}
Explicación
- Double-checked locking: Después del primer chequeo exitoso, otro thread podría haber inicializado la instancia entre el chequeo y el lock, así que el segundo chequeo dentro del bloque sincronizado es necesario.
- Singleton a nivel de módulo (Python): los módulos de Python se importan una vez y se cachean en
sys. modules. Una clase definida en un módulo e instanciada a nivel de módulo se comporta como singleton. Todos los imports referencian el mismo objeto. Es más simple que__new__pero menos explícito. - Patrón registro: en lugar de hardcodear
getInstance()en cada clase, un registro central mapea claves a instancias singleton. Esto desacopla la creación de la clase, soporta singletons parametrizados y permite reset fácil para testing. El registro mismo es un singleton. - Singleton con DI container: frameworks modernos (Spring, ASP. NET, Angular) gestionan el ciclo de vida de singletons declarativamente. Declaras un binding como singleton scope y el container crea una instancia que inyecta en todas partes. Es el enfoque más testeable — los tests usan un container separado con mocks.
Variantes
| Enfoque | Thread-safe | Perezoso | Testeable | Mejor para |
|---|---|---|---|---|
| Estático eager | Sí | No | Pobre | Recursos simples, siempre necesarios |
| Double-checked lock | Sí | Sí | Pobre | Inicialización perezosa performance-crítica |
| Bill Pugh (holder) | Sí | Sí | Pobre | Enfoque preferido en Java |
| Enum singleton | Sí | No | Pobre | Singleton basado en enum Java |
| A nivel de módulo | Sí* | Sí | Pobre | Casos simples en Python |
| Registro | Sí | Sí | Bueno | Múltiples singletons nombrados |
| DI container | Sí | Sí | Excelente | Aplicaciones modernas |
Lo que funciona
- Prefiere DI sobre singletons manuales: un container de inyección de dependencias gestiona singletons declarativamente. Configuras `services. Las dependencias son explícitas y el testing es trivial.
- Haz singletons stateless o inmutables: un singleton mutable es estado global, y el estado global es el enemigo del testing y la concurrencia. Consulta Prevención de Race Conditions para seguridad concurrente.
- Evita singletons para lógica de negocio: un
UserServiceno debería ser singleton. Las reglas de negocio cambian por request (usuarios distintos, contextos distintos). Reserva singletons para infraestructura: pools de conexiones, caches, loggers, lectores de configuración. - Implementa IDisposable / Closeable: un singleton frecuentemente mantiene recursos (conexiones, threads, file handles). En Spring o ASP. NET, registra hooks de disposición con el container.
- Documenta thread-safety: si el singleton no es thread-safe, documentalo claramente. Los consumidores deben sincronizar externamente.
Errores comunes
- Testing con singletons mutables: un test que llama
Config. setDebug(true)filtra ese setting a todos los tests subsecuentes. Pasa configuración como parámetros de constructor. - Inicialización perezosa en código multithread sin sincronización: dos threads llamando
getInstance()simultáneamente pueden crear dos instancias antes de que alguna asigne al campo estático. - Singletons con estado de ámbito de request: Los singletons deben mantener solo datos de ámbito aplicación.
- Dependencias circulares en singletons: si
ConnectionPooles singleton que depende deConfigManager, yConfigManageres singleton que depende deConnectionPool, ninguno puede construirse. Los containers DI detectan esto y lanzan excepciones, pero los singletons manuales se bloquean durante la inicialización estática.
Errores Comunes Adicionales
-
Usar singleton para compartir estado entre microservicios. Cada microservicio tiene su propia JVM/proceso — un singleton en uno no es visible para otro. Usa un cache distribuido (Redis) o base de datos compartida.
-
Singleton con colecciones mutables sin sincronización. Un singleton con un
HashMapque múltiples threads leen y escriben se corromperá. UsaConcurrentHashMapo sincroniza el acceso:
// Mal: race condition en HashMap
private Map<String, User> cache = new HashMap<>();
// Bien: thread-safe
private Map<String, User> cache = new ConcurrentHashMap<>();
- Olvidar cerrar recursos singleton en shutdown. Los pools de conexiones y thread executors filtran si no se cierran:
// Hook de shutdown
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
DatabaseConnectionPool.getInstance().close();
})); Preguntas frecuentes
Bill Pugh Holder Idiom (Java)
public class ConfigManager {
private ConfigManager() {
// Cargar config desde entorno o archivo
}
// Clase interna estática — cargada solo cuando getInstance() se llama
private static class Holder {
static final ConfigManager INSTANCE = new ConfigManager();
}
public static ConfigManager getInstance() {
return Holder.INSTANCE;
}
public String get(String key) {
// ...
return "";
}
}
La JVM garantiza que una clase se inicializa exactamente una vez, y la clase holder no se carga hasta que getInstance() se llama por primera vez. Esto da inicialización perezosa sin overhead de sincronización — la JVM maneja la thread safety.
Enum Singleton (Java)
public enum DatabaseType {
INSTANCE;
private final Map<String, String> properties;
DatabaseType() {
this.properties = loadProperties();
}
public String getProperty(String key) {
return properties.get(key);
}
private Map<String, String> loadProperties() {
// Cargar desde archivo o env
return Map.of("driver", "postgresql");
}
}
// Uso
String driver = DatabaseType.INSTANCE.getProperty("driver");
Los enum singletons están garantizados por la JVM como instancias únicas, incluso a través de serialización y reflection. Esta es la forma más robusta de singleton en Java.
Testing de Código Singleton
// Singleton testeable vía registro — reset entre tests
describe('OrderService con singleton pool', () => {
afterEach(() => {
SingletonRegistry.clear();
});
it('usa pool de conexiones compartido', () => {
const pool = SingletonRegistry.get('pool', () => new InMemoryConnectionPool());
const service = new OrderService(pool);
service.process({ id: '1', items: [] });
expect(pool.getConnectionsUsed()).toBe(1);
});
it('aísla estado entre tests', () => {
// Registro fue limpiado — nueva instancia de pool
const pool = SingletonRegistry.get('pool', () => new InMemoryConnectionPool());
expect(pool.getConnectionsUsed()).toBe(0);
});
});
// Java — test con DI container
@Test
void testOrderServiceWithMockPool() {
var container = new DIContainer();
container.bind(IDatabaseConnectionPool.class, MockConnectionPool.class, Scope.SINGLETON);
var service = container.resolve(OrderService.class);
service.process(new Order("1", List.of()));
var mockPool = container.resolve(IDatabaseConnectionPool.class);
verify(mockPool, times(1)).getConnection();
}
Recursos Relacionados
Crear Objetos Flexiblemente con el Factory Pattern
Cómo usar factory methods, abstract factories y containers de inyección de dependencias para desacoplar creación de objetos de su uso y mejorar testeabilidad.
RecipeConstruir Aplicaciones Mantenibles con Arquitectura
Cómo estructurar aplicaciones usando ports y adapters para aislar lógica de negocio de frameworks, bases de datos y servicios externos para testabilidad y flexibilidad.
RecipeEscribir Unit Tests con Mocks y Stubs
Cómo aislar código bajo test usando objetos mock, stubs y spies para reemplazar dependencias externas como bases de datos, APIs y sistemas de archivos.