StackPractices
beginner Por Mathias Paulenko

Patrón Singleton: Ejemplos en Java, Python y TypeScript

Garantiza que una clase tenga una única instancia y ofrece acceso global a ella — el patrón creacional para controlar la creación de objetos, con implementaciones listas para producción.

Temas: design

Visión general

Tu aplicación acaba de abrir un segundo pool de conexiones a la base de datos y ahora cada consulta compite por la mitad de las conexiones. Ese es el problema que resuelve Singleton: algunos recursos deben existir exactamente una vez — un pool de conexiones, un lector de configuración, un logger — y el patrón Singleton garantiza esa instancia única más una forma controlada de acceder a ella.

El patrón viene de la Gang of Four (Design Patterns, 1994) y es el patrón creacional más usado — y más abusado — de todos. Esta página cubre las implementaciones que de verdad sobreviven en producción: inicialización lazy y eager, double-checked locking con volatile, el idiom de Bill Pugh, el enum singleton de Java, singletons a nivel de módulo en Python y Node, registros y la alternativa de inyección de dependencias que querrás para todo lo testeable.

Cuándo usarlo

Usa Singleton cuando se cumplan todas estas condiciones:

  • Debe existir exactamente una instancia — un pool de conexiones, una caché en memoria, un gestor de configuración, una factoría de loggers.
  • Varias instancias entrarían en conflicto o desperdiciarían recursos (un segundo pool duplica las conexiones a la base de datos).
  • Quieres inicialización diferida para posponer una preparación costosa hasta el primer uso.

No lo uses cuando:

  • El objeto guarda estado mutable de negocio — eso es estado global con pasos extra. Pásalo como parámetro.
  • Necesitas datos por usuario o por petición — un singleton con estado por petición es una fuga de memoria.
  • La testabilidad pesa más que la comodidad — prefiere la inyección de dependencias, que da la misma instancia única con un contrato explícito.

Solución

Java — las implementaciones que importan

Empieza con el enum singleton. Son tres líneas, es thread-safe por garantía de la JVM y es la única variante inmune a ataques de serialización y reflexión (recomendación de Joshua Bloch en Effective Java, ítem 3):

public enum Configuration {
    INSTANCE;

    private final Map<String, String> properties = loadProperties();

    public String get(String key) {
        return properties.get(key);
    }

    private Map<String, String> loadProperties() {
        return Map.of("env", System.getenv("APP_ENV"));
    }
}

// Uso — la misma instancia en todas partes, imposible de duplicar
String env = Configuration.INSTANCE.get("env");

Si no puedes usar un enum (necesitas herencia), el idiom del holder de Bill Pugh da lazy init sin coste de sincronización — la JVM solo carga la clase interna en el primer acceso:

public class ConnectionPool {
    private ConnectionPool() {
        // preparación costosa: abrir pool, enlazar sockets
    }

    private static class Holder {
        static final ConnectionPool INSTANCE = new ConnectionPool();
    }

    public static ConnectionPool getInstance() {
        return Holder.INSTANCE;
    }
}

Y si necesitas construcción condicional o configurable, el double-checked locking funciona — pero solo con volatile; sin él, un segundo hilo puede leer una instancia construida a medias:

public class CacheClient {
    private static volatile CacheClient instance;

    private CacheClient() {}

    public static CacheClient getInstance() {
        if (instance == null) {                     // camino rápido, sin lock
            synchronized (CacheClient.class) {
                if (instance == null) {             // segunda comprobación dentro del lock
                    instance = new CacheClient();
                }
            }
        }
        return instance;
    }
}

El synchronized getInstance() ingenuo que ves en tutoriales antiguos es correcto pero más lento — bloquea en cada llamada, no solo en la primera.

Python — prefiere el módulo, no __new__

El singleton clásico con __new__ tiene dos trampas que nadie te cuenta: __init__ se ejecuta de nuevo en cada llamada (reinicializando tu estado) y el check-then-act dentro de __new__ no es atómico — el GIL no te salva aquí; dos hilos pueden seguir compitiendo entre la comprobación de None y la asignación. Si insistes en __new__, protégelo con un lock:

import threading

class ConnectionPool:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        if self._initialized:
            return                          # no reejecutar la preparación
        self._pool = self._create_pool()
        self._initialized = True

El idiom más limpio de Python es el propio módulo: los módulos se importan una vez y se cachean en sys.modules, así que una instancia a nivel de módulo es un singleton gratis — thread-safe, lazy en la primera importación y el enfoque recomendado:

# connection_pool.py
import psycopg2.pool

pool = psycopg2.pool.ThreadedConnectionPool(
    minconn=2, maxconn=10,
    dsn="postgresql://user:pass@localhost/db",
)

# Todo importador recibe el mismo objeto:
from connection_pool import pool

TypeScript — constructores privados y registros

Un constructor privado bloquea new y un accesor estático controla la única instancia. Para cualquier cosa más allá del caso más simple, un registro con nombres es más útil — mapea claves a instancias, así que obtienes “una por nombre” (un Multiton, de hecho) más un hook de reset para tests:

class Registry {
  private static instances = new Map<string, unknown>();

  static get<T>(key: string, factory: () => T): T {
    if (!Registry.instances.has(key)) {
      Registry.instances.set(key, factory());
    }
    return Registry.instances.get(key) as T;
  }

  static reset(key: string): void {
    Registry.instances.delete(key);
  }
}

const pool = Registry.get('db-pool', () => createPool(config));
Registry.reset('db-pool'); // en tests, entre casos

Node.js también te da singletons gratis igual que Python: require/import cachea módulos, así que una instancia exportada la comparte todo importador.

Diagrama

classDiagram diagram: class Singleton {

Explicación

Toda implementación de Singleton responde las mismas tres preguntas:

  • ¿Quién bloquea new? Un constructor privado (Java/TS), un enum o el sistema de módulos del lenguaje (Python/Node).
  • ¿Cuándo se crea la instancia? Eager en la carga, o lazy en el primer acceso — lazy ahorra coste de arranque pero introduce el problema de thread-safety.
  • ¿Quién impone la instancia única? La propia clase, un registro o el contenedor DI.

Sobre thread-safety: la carrera siempre está en la inicialización lazy. Dos hilos llegan a if (instance == null) a la vez, ambos construyen y el segundo sobrescribe al primero. synchronized lo arregla de forma ingenua; volatile + double-checked locking mantiene el camino rápido sin bloqueos; el idiom del holder lo delega a las garantías de carga de clases. En Java hay un detalle más sutil — sin volatile, un hilo puede ver una referencia no nula a un objeto cuyo constructor no ha terminado de escribir los campos, porque la JVM puede reordenar los pasos “reservar → construir → asignar”.

Variantes

VarianteLazyThread-safeTesteableCuándo gana
Estática eagerNoSíPobreInicialización barata, siempre necesaria
Accesor sincronizadoSíSíPobreSimple, poco tráfico
Double-checked lockingSíSí*PobreRuta caliente, Java/C# — necesita volatile
Holder de Bill PughSíSíPobreLazy init en Java, sin coste de lock
Enum singletonNoSíPobreJava — sobrevive serialización y reflexión
A nivel de móduloSíSíPobrePython, Node — idiomático, cero maquinaria
Registro / MultitonSíSíBuenaInstancias con nombre, hooks de reset
Contenedor DISíSíExcelenteTodo lo que necesites mockear

*Solo con volatile; sin él el check-then-act sigue roto.

Singleton en tests — la parte que los tutoriales se saltan

Un singleton con getInstance() hace los tests dependientes del orden: lo que un test muta se filtra al siguiente. Tres salidas:

// 1. Registro con reset — limpiar entre tests
afterEach(() => Registry.reset('db-pool'));

// 2. Contenedor DI — vincular un mock para la corrida de tests
container.bind(IPool).to(MockPool).inSingletonScope();
const service = container.get(OrderService);

// 3. Inyección por constructor — pasar la instancia explícitamente
const service = new OrderService(pool); // pool es el singleton en prod, un fake en tests

La respuesta honesta es la opción 3. Si alguna parte del código podría necesitar una segunda implementación en tests, inyecta la dependencia y deja el singleton solo como vinculación de producción.

Serialización, reflexión y classloaders

Tres formas en que Singleton se rompe en silencio, todas reales:

  • Serialización (Java): deserializar crea una instancia nueva aunque el constructor sea privado. Arréglalo con readResolve():
    protected Object readResolve() {
        return getInstance();
    }
    O usa un enum singleton, que lo resuelve automáticamente.
  • Reflexión: setAccessible(true) puede llamar a tu constructor privado. Los enum singletons son inmunes — la JVM garantiza una única constante.
  • Varios classloaders: en servidores de aplicaciones y sistemas de plugins, la misma clase cargada por dos classloaders produce dos “singletons”. Los sistemas de módulos (Python, Node) tienen el problema equivalente con copias duplicadas del paquete. Si ves dos instancias, es el primer sitio donde mirar — System.identityHashCode() o id() lo confirmarán.

Alternativas

AlternativaObtienesPierdes
Contenedor DI (Spring, ASP.NET, Nest)Ciclo de vida singleton + testabilidad + contratos explícitosDependencia del framework
Sistema de módulos (Python, Node)Singleton idiomático gratisDifícil de cambiar en tests
MonostateEstado compartido, múltiples instanciasLos mismos riesgos de estado global
Clase estáticaLo más simple posibleSin polimorfismo, sin mocks, sin inyección

Singleton frente a clase estática merece una línea extra: un singleton es un objeto — puede implementar una interfaz, pasarse como parámetro y sustituirse en tests. Una clase estática no puede hacer nada de eso.

Lo que funciona

  • Mantén el estado del singleton inmutable o de solo lectura tras inicializarlo — el estado global mutable es donde viven los bugs de verdad.
  • En Java: enum o Bill Pugh primero; son correctos sin que tengas que demostrarlo.
  • En Python/Node: usa el sistema de módulos; no reinventes getInstance().
  • Deja por escrito cómo se testea — un reset() del registro o una vinculación DI — antes de que el primer compañero tropiece.
  • Documenta qué posee el singleton (conexiones, caché) y cómo se libera al apagar.

Errores comunes

  • Asumir que __new__ es thread-safe por el GIL — no lo es; usa un lock o inicialización a nivel de módulo.
  • Olvidar que __init__ se reejecuta en cada llamada Singleton() en Python y reinicializa el estado.
  • Double-checked locking sin volatile — parece correcto, falla en producción.
  • Un singleton con datos por petición o por usuario — es una fuga de memoria disfrazada de patrón.
  • Servicios de lógica de negocio como singletons — un UserService singleton es una bomba de acoplamiento; inyéctalo.
  • Saltarse el cierre — los pools y ejecutores fugan si nadie llama a close() al salir.

Lectura adicional

Preguntas frecuentes

¿Es el patrón Singleton un antipatrón?

El Singleton en sí no es el problema — el estado global mutable sí. Un singleton para configuración inmutable o un pool de conexiones es infraestructura legítima. Un singleton para estado de negocio hace los tests dependientes del orden y acopla todo con todo.

¿Cómo hago un singleton thread-safe en Python?

No confíes en el GIL — el check-then-act dentro de __new__ puede seguir compitiendo. Protege __new__ con threading.Lock o, mejor, usa una instancia a nivel de módulo: los módulos de Python se importan una vez y se cachean, así que son singletons thread-safe por construcción.

¿Cuál es la mejor implementación de singleton en Java?

El enum singleton. Es thread-safe por garantía de la JVM, lazy en la carga de clase e inmune a ataques de serialización y reflexión — la única variante que lo es. Si necesitas lazy init con una clase normal, usa el idiom del holder de Bill Pugh.

¿Cómo testeo código que depende de un singleton?

La mejor respuesta: no llames a getInstance() dentro de la lógica de negocio — inyecta la dependencia por el constructor y vincula el singleton en el contenedor DI. Si estás atado a getInstance(), usa una variante de registro con un hook reset() para que cada test reciba una instancia fresca.

¿Singleton frente a clase estática — cuándo importa?

Un singleton es un objeto: puede implementar interfaces, inyectarse y mockearse. Una clase estática es solo un namespace para funciones. Si podrías necesitar polimorfismo o dobles de test, usa un singleton; si es utilidad pura sin estado, una clase estática es más simple.

¿El double-checked locking sigue necesitando volatile?

Sí, en Java. Sin volatile un hilo puede observar una referencia no nula a un objeto parcialmente construido — la JVM puede publicar la referencia antes de que termine el constructor. Es el detalle de implementación más roto de todos.

¿Se puede serializar un singleton?

Ingenuamente, sí — y la deserialización creará una segunda instancia. Implementa readResolve() para devolver la instancia existente, o usa un enum singleton, que lo resuelve solo.

¿Qué diferencia hay entre Singleton y Multiton?

Un Singleton impone exactamente una instancia; un Multiton impone una instancia por clave — un mapa de singletons con nombre. Los registros son Multitons en la práctica.