🚨 ¡Nueva review! 🔇 Los mejores cascos con ANC del mercado: los Sony WH-1000XM4 . ¡Échale un ojo! 👀

Patrón Singleton: la única instancia que lo controla todo (y por qué hay que tener cuidado)

Una sola instancia, acceso global y muchas opiniones. Entiéndelo bien antes de usarlo.

Escrito por domin el 11 de septiembre de 2025 · Actualizado el 8 de febrero de 2026

🍰 Patrón Singleton: la receta para tener solo una cosa

Imagina que en tu casa solo hay una única llave para abrir la puerta principal.

Esa llave única es el Singleton. Un objeto que solo puede existir una vez y todos los que necesitan acceder a él deben usar esa misma instancia.


¿Qué es el Singleton?

El Singleton es un patrón de diseño creacional del catálogo Gang of Four (GoF) que garantiza que una clase tenga una y solo una instancia en toda la aplicación, y proporciona un punto de acceso global a esa instancia.

Imagina que en tu aplicación necesitas que solo exista una cosa de algo específico: una sola conexión a la base de datos, un solo logger para escribir logs, o una sola configuración global. El Singleton es como tener un solo control remoto para la tele. No importa quién lo use, todos van al mismo.

Los 3 pilares del Singleton

1. Constructor privado

Nadie puede hacer new desde fuera.

2. Instancia estática

La clase guarda su propia y única instancia.

3. Método getInstance()

El único punto de acceso. Crea la instancia si no existe, la devuelve si ya existe.


Analogías de la vida real

Antes de meternos con código, unas analogías para que el concepto quede claro:

Todos estos comparten la misma idea: una sola instancia con acceso compartido.


Implementación básica en PHP

<?php

class DatabaseConnection
{
    private static ?self $instance = null;
    private PDO $connection;

    // Constructor privado - nadie puede crear instancias desde fuera
    private function __construct()
    {
        $this->connection = new PDO(
            'mysql:host=localhost;dbname=test',
            'user',
            'pass'
        );
    }

    // El método mágico para obtener LA única instancia
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function getConnection(): PDO
    {
        return $this->connection;
    }

    // Evitamos que alguien clone la instancia
    private function __clone() {}

    // Evitamos deserialización
    public function __wakeup()
    {
        throw new \Exception("No se puede deserializar un Singleton");
    }
}

// Uso:
$db1 = DatabaseConnection::getInstance();
$db2 = DatabaseConnection::getInstance();

// $db1 y $db2 son exactamente la misma instancia!
var_dump($db1 === $db2); // true

Fíjate en los detalles clave:


Casos de uso reales

1. Configuración global

<?php

class AppConfig
{
    private static ?self $instance = null;
    private array $config;

    private function __construct()
    {
        $this->config = [
            'app_name' => 'Mi Super App',
            'version'  => '1.0.0',
            'debug'    => true,
            'timezone' => 'Europe/Madrid',
        ];
    }

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function get(string $key): mixed
    {
        return $this->config[$key] ?? null;
    }

    public function set(string $key, mixed $value): void
    {
        $this->config[$key] = $value;
    }

    private function __clone() {}
}

// Uso en cualquier parte de la aplicación:
$config = AppConfig::getInstance();
echo $config->get('app_name'); // "Mi Super App"

// En otra parte completamente diferente del código:
$config2 = AppConfig::getInstance();
$config2->set('debug', false);

// Los cambios se reflejan en todos lados porque es la MISMA instancia
echo $config->get('debug'); // false

2. Cache simple

<?php

class SimpleCache
{
    private static ?self $instance = null;
    private array $cache = [];
    private array $ttl = [];

    private function __construct() {}

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function set(string $key, mixed $value, int $seconds = 3600): void
    {
        $this->cache[$key] = $value;
        $this->ttl[$key] = time() + $seconds;
    }

    public function get(string $key): mixed
    {
        if (!isset($this->cache[$key])) {
            return null;
        }

        if (time() > $this->ttl[$key]) {
            unset($this->cache[$key], $this->ttl[$key]);
            return null;
        }

        return $this->cache[$key];
    }

    public function has(string $key): bool
    {
        return $this->get($key) !== null;
    }

    public function clear(): void
    {
        $this->cache = [];
        $this->ttl = [];
    }

    private function __clone() {}
}

// En cualquier lugar de tu app:
$cache = SimpleCache::getInstance();
$cache->set('user_name', 'Juan', 300); // 5 minutos

// En otra parte:
$cache = SimpleCache::getInstance();
echo $cache->get('user_name'); // "Juan" (misma instancia)

3. Logger

<?php

class Logger
{
    private static ?self $instance = null;
    private string $logFile;

    private function __construct()
    {
        $this->logFile = '/var/log/app.log';
    }

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function info(string $message): void
    {
        $this->write('INFO', $message);
    }

    public function error(string $message): void
    {
        $this->write('ERROR', $message);
    }

    public function warning(string $message): void
    {
        $this->write('WARNING', $message);
    }

    private function write(string $level, string $message): void
    {
        $timestamp = date('Y-m-d H:i:s');
        $line = "[$timestamp] [$level] $message" . PHP_EOL;
        file_put_contents($this->logFile, $line, FILE_APPEND);
    }

    private function __clone() {}
}

// En cualquier parte:
Logger::getInstance()->info("Usuario logueado");
Logger::getInstance()->error("Fallo en la conexión");

El problema del Thread Safety

El código anterior tiene un problema importante: no es thread-safe. En entornos con múltiples hilos, podrían crearse varias instancias al mismo tiempo:

// ⚠️ Problema: En concurrencia, esto puede fallar
public static function getInstance(): self
{
    // Hilo A llega aquí y comprueba: $instance === null → true
    // Hilo B llega aquí al mismo tiempo y comprueba: $instance === null → true
    if (self::$instance === null) {
        // ¡Ambos hilos crean una instancia nueva!
        self::$instance = new self();
    }
    return self::$instance;
}

La solución es el patrón Double-Checked Locking:

<?php

class ThreadSafeConnection
{
    private static ?self $instance = null;
    private static bool $lock = false;

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            while (self::$lock) {
                usleep(1);
            }

            self::$lock = true;
            if (self::$instance === null) { // Double check
                self::$instance = new self();
            }
            self::$lock = false;
        }
        return self::$instance;
    }

    private function __construct() {}
    private function __clone() {}
}

Nota: En PHP esto es menos crítico porque tradicionalmente es single-threaded por request. Cada petición HTTP crea su propio proceso y su propio espacio de memoria. Pero es importante entender el concepto porque en otros lenguajes (Java, C#, Go…) el thread safety del Singleton es un tema muy serio.


Por qué el Singleton se considera un anti-patrón

Aquí viene lo polémico. El Singleton puede ser útil en casos muy específicos, pero la comunidad de desarrollo lo considera un anti-patrón por razones bastante sólidas:

Los problemas
  • Dificulta el testing: Crea dependencias ocultas difíciles de mockear.
  • Viola el SRP: La clase maneja su creación Y su lógica de negocio.
  • Acoplamiento fuerte: Las clases se acoplan directamente al Singleton.
  • Estado global oculto: Puede crear efectos secundarios inesperados.
  • Dificulta el paralelismo: Problemas de concurrencia y sincronización.
Las ventajas
  • Garantiza una instancia: Imposible tener duplicados.
  • Acceso global: Disponible en toda la aplicación.
  • Lazy initialization: Se crea solo cuando se necesita.
  • Simple: Fácil de entender e implementar.
  • Ahorra recursos: Una conexión en vez de muchas.

El problema del testing explicado

Este es el problema más gordo y el que más importa en el día a día:

<?php

// ❌ Difícil de testear - dependencias ocultas
class OrderProcessor
{
    public function processOrder(array $orderData): void
    {
        $logger = Logger::getInstance();      // ← Dependencia oculta!
        $config = AppConfig::getInstance();   // ← No se puede mockear fácil
        $db = DatabaseConnection::getInstance(); // ← Acoplamiento directo

        // ¿Cómo testeas esto sin un logger real,
        // una config real y una base de datos real?
    }
}

// ✅ Fácil de testear - dependencias explícitas e inyectadas
class OrderProcessor
{
    public function __construct(
        private LoggerInterface $logger,
        private ConfigInterface $config,
        private ConnectionInterface $db
    ) {}

    public function processOrder(array $orderData): void
    {
        // En tests puedes inyectar mocks fácilmente
        // Sin base de datos real, sin logger real, sin config real
    }
}

Con el Singleton, las dependencias están escondidas dentro de los métodos. Quien lee el constructor de OrderProcessor no sabe que depende de un logger, una config y una base de datos. Con inyección de dependencias, todo está explícito en el constructor. Transparente y testeable.


Alternativas modernas

En lugar del Singleton clásico, existen alternativas más limpias que resuelven el mismo problema sin sus desventajas:

1. Dependency Injection (DI)

La alternativa más recomendada. En lugar de que la clase busque su dependencia, se la pasas desde fuera:

<?php

class UserService
{
    public function __construct(
        private PDO $db,
        private LoggerInterface $logger
    ) {}

    // No necesitas getInstance() en ningún lado
    // Las dependencias llegan por constructor
}

2. Service Containers (Contenedores IoC)

Los frameworks modernos (Symfony, Laravel, etc.) manejan instancias únicas de forma elegante. Tú registras un servicio como singleton en el contenedor, y el framework se encarga de crear una sola instancia y de inyectarla donde haga falta:

// Laravel - registrar como singleton
$this->app->singleton(DatabaseConnection::class, function () {
    return new DatabaseConnection(config('database'));
});

// Symfony - los servicios son singleton por defecto
// En services.yaml no necesitas hacer nada especial

// El framework inyecta automáticamente la misma instancia
class UserController
{
    public function __construct(
        private DatabaseConnection $db // ← Siempre la misma instancia
    ) {}
}

Esto te da lo mejor de ambos mundos: una sola instancia (como el Singleton) pero con inyección de dependencias (sin los problemas del Singleton).

3. Factory Pattern

Si no tienes un framework con contenedor, un Factory es una alternativa más limpia:

<?php

class DatabaseFactory
{
    private static ?PDO $connection = null;

    public static function createConnection(): PDO
    {
        if (self::$connection === null) {
            self::$connection = new PDO(
                'mysql:host=localhost;dbname=test',
                'user',
                'pass'
            );
        }
        return self::$connection;
    }
}

// La responsabilidad de gestionar la instancia está en la factory,
// no en la clase de conexión. Más limpio.

¿Cuándo SÍ usar Singleton?

Úsalo solo cuando se cumplan estas condiciones:

Casos legítimos:

¿Cuándo NO usar Singleton?


Singleton vs Dependency Injection: la comparativa

AspectoSingletonDependency Injection
TestabilidadDifícil de mockearFácil de mockear
DependenciasOcultas dentro del códigoExplícitas en el constructor
AcoplamientoAlto (clase concreta)Bajo (interfaz)
FlexibilidadUna implementación fijaIntercambiable
ComplejidadSimple de implementarRequiere contenedor o config
Instancia únicaGarantizada por la claseGestionada por el contenedor

Conclusiones nene, conclusiones

El Singleton es como tener una sola llave para abrir todas las puertas de un tipo específico. Es una herramienta que:

Si tienes dudas sobre usar Singleton, probablemente no deberías usarlo. Las alternativas modernas como Dependency Injection suelen ser mejores en el 95% de los casos. Pero entiéndelo bien, porque te lo van a preguntar en entrevistas y porque a veces, en scripts pequeños o código legacy, es la opción más pragmática.

EA nos leemos! 🍻


Pon a prueba lo aprendido

1. ¿Qué tipo de patrón de diseño es el Singleton?

2. ¿Cuáles son los 3 pilares de la implementación del Singleton?

3. ¿Por qué se hace privado el constructor en un Singleton?

4. ¿Cuál es el problema principal del Singleton respecto al testing?

5. ¿Qué principio SOLID viola el Singleton?

6. ¿Cuál es la alternativa más recomendada al Singleton?

7. ¿Qué es el Double-Checked Locking en un Singleton?

8. ¿En cuál de estas situaciones SÍ tendría sentido usar Singleton?