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

El Patrón de diseño Proxy

Controla el acceso al objeto real de forma inteligente.

Escrito por domin el 4 de noviembre de 2025 · Actualizado el 15 de febrero de 2026

🛡️ El Patrón Proxy: El Intermediario que lo peta

El Patrón de diseño Proxy tiene un objetivo superútil: proporcionar un sustituto o un marcador de posición para otro objeto. No es que haga el trabajo del objeto, sino que se pone delante de él para controlar quién y cómo puede acceder a sus funcionalidades.

¿Cómo puedes controlar el acceso a un objeto grande o sensible sin modificar su código, o cómo puedes optimizar su uso (como hacer una caché) sin que el cliente se dé cuenta?

La respuesta es creando un objeto Proxy que comparte la misma interfaz que el objeto original, y así, el cliente interactúa con el Proxy pensando que es el objeto real.

Diagrama del Patrón Proxy mostrando el cliente interactuando con el proxy, y este con el objeto real.

🛡️ 1. El portero de discoteca

Imagina que quieres entrar en una discoteca exclusiva (La discoteca sería el objeto real):

El tema está en que el Proxy implementa la misma interfaz que el Objeto Real, así que para el cliente, son indistinguibles. Puedes cambiar un objeto por su proxy y el código del cliente seguirá funcionando.

¿Cuál es el objetivo principal del Patrón Proxy?


🛠️ 2. Los roles del patrón

Sujeto (Interfaz Común)

Define los métodos que tanto el Objeto Real como el Proxy deben implementar. Es lo que permite que el Proxy sea un sustituto transparente.

Sujeto Real

La clase que hace el trabajo de verdad. Puede ser lenta, costosa en recursos o sensible. El Proxy la protege o la optimiza.

Proxy (Sustituto)

Implementa la misma interfaz que el Real. Contiene una referencia al Real y añade lógica de control antes o después de delegarle la llamada.

El cliente nunca sabe si habla con el Proxy o con el Real. Ambos implementan la misma interfaz, así que son intercambiables.


🧑‍💻 3. Ejemplo práctico en PHP: Servicio de descargas con control de acceso

Vamos a implementar un servicio donde la descarga tarda mucho (el Objeto Real) y queremos que solo usuarios premium puedan descargar. El Proxy se pone delante y verifica permisos antes de dejar pasar.

<?php

// 1. El Sujeto (Interfaz Común)
interface ServicioDeDescarga
{
    public function descargar(string $archivo, string $usuario): bool;
}

// 2. El Sujeto Real (El objeto que hace el trabajo pesado)
class LibreriaDeDescargasPesadas implements ServicioDeDescarga
{
    public function descargar(string $archivo, string $usuario): bool
    {
        echo "   [REAL] >> Conectando a servidor, proceso lento...\n";
        sleep(2); // Simula descarga pesada
        echo "   [REAL] Archivo '$archivo' descargado para '$usuario'.\n";
        return true;
    }
}

// 3. El Proxy de Protección
class ProxyDeDescargaPremium implements ServicioDeDescarga
{
    private LibreriaDeDescargasPesadas $servicioReal;
    private array $usuariosPremium = ['cliente@premium.com', 'otro@vip.com'];

    public function __construct(LibreriaDeDescargasPesadas $servicioReal)
    {
        $this->servicioReal = $servicioReal;
    }

    public function descargar(string $archivo, string $usuario): bool
    {
        echo "➡️ [PROXY] Verificando permisos...\n";

        // 1. Control de acceso (valor añadido del Proxy)
        if (!in_array($usuario, $this->usuariosPremium)) {
            echo "❌ ACCESO DENEGADO: '$usuario' no es Premium.\n";
            return false;
        }

        echo "✅ '$usuario' es Premium. Delegando al servicio real...\n";

        // 2. Log antes de delegar
        $inicio = microtime(true);

        // 3. Delegación al Objeto Real
        $resultado = $this->servicioReal->descargar($archivo, $usuario);

        // 4. Log después de delegar
        $tiempo = round(microtime(true) - $inicio, 2);
        echo "   [PROXY] Descarga completada en {$tiempo}s\n";

        return $resultado;
    }
}

// 4. Uso (el cliente)
$servicioReal = new LibreriaDeDescargasPesadas();
$proxy = new ProxyDeDescargaPremium($servicioReal);

echo "--- Usuario NO Premium ---\n";
$proxy->descargar('documento_secreto.zip', 'anonimo@gratis.com');

echo "\n--- Usuario PREMIUM ---\n";
$proxy->descargar('pelicula_4k.mkv', 'cliente@premium.com');

// Salida:
// --- Usuario NO Premium ---
// ➡️ [PROXY] Verificando permisos...
// ❌ ACCESO DENEGADO: 'anonimo@gratis.com' no es Premium.
//
// --- Usuario PREMIUM ---
// ➡️ [PROXY] Verificando permisos...
// ✅ 'cliente@premium.com' es Premium. Delegando al servicio real...
//    [REAL] >> Conectando a servidor, proceso lento...
//    [REAL] Archivo 'pelicula_4k.mkv' descargado para 'cliente@premium.com'.
//    [PROXY] Descarga completada en 2.01s

El Proxy no solo comprueba permisos (control de acceso), sino que también mide el tiempo de la operación (logging). Un proxy puede combinar varias responsabilidades antes y después de delegar.


🏷️ 4. Tipos de Proxy

No todos los proxies son iguales. Dependiendo de qué lógica añadan, se clasifican en varios tipos:

Proxy de Protección (Protection)

Controla quién puede acceder al objeto real. Verifica permisos, roles, autenticación. El portero de discoteca del ejemplo.

Proxy Virtual (Lazy Loading)

Retrasa la creación del objeto real hasta que sea estrictamente necesario. Si el objeto es pesado de instanciar, el proxy evita hacerlo hasta el último momento.

Proxy de Caché

Guarda resultados de operaciones previas y los devuelve directamente si se pide lo mismo. Evita repetir llamadas costosas al objeto real.

Proxy de Logging

Registra cada llamada al objeto real: quién llamó, cuándo, con qué parámetros, cuánto tardó. Sin ensuciar la lógica del Real.

Y hay más, como el Proxy Remoto (accede a un objeto en otra máquina, como los stubs de RPC), Proxy de Smart Reference (mantiene un contador de referencias), etc. Pero los cuatro de arriba son los que te encontrarás en el día a día.

¿Qué tipo de Proxy retrasa la creación del objeto real hasta que se necesita?


🧑‍💻 5. Ejemplo avanzado: Proxy de Caché + Virtual

Vamos a implementar un proxy que combina lazy loading (no instancia el servicio real hasta la primera llamada) y caché (no repite consultas que ya hizo):

<?php

interface ServicioClima
{
    public function obtenerTemperatura(string $ciudad): float;
}

// El servicio real: llama a una API externa (lento y costoso)
class ServicioClimaAPI implements ServicioClima
{
    public function __construct()
    {
        echo "   🌐 [REAL] Inicializando conexión con API externa...\n";
        // Simula inicialización costosa (pool de conexiones, auth, etc.)
        sleep(1);
    }

    public function obtenerTemperatura(string $ciudad): float
    {
        echo "   🌐 [REAL] Consultando API para '$ciudad'...\n";
        sleep(1); // Simula latencia de red
        return round(rand(50, 400) / 10, 1); // Temperatura simulada
    }
}

// Proxy que combina Virtual (lazy) + Caché
class ProxyClimaConCache implements ServicioClima
{
    private ?ServicioClimaAPI $servicioReal = null; // null = no instanciado aún
    private array $cache = [];
    private int $ttl; // Tiempo de vida en segundos

    public function __construct(int $ttl = 300)
    {
        $this->ttl = $ttl;
        echo "   [PROXY] Proxy creado (servicio real NO instanciado aún)\n";
    }

    public function obtenerTemperatura(string $ciudad): float
    {
        // 1. Comprobar caché
        if ($this->enCache($ciudad)) {
            echo "   ⚡ [PROXY] Cache HIT para '$ciudad'\n";
            return $this->cache[$ciudad]['valor'];
        }

        // 2. Lazy loading: instanciar el real solo cuando hace falta
        if ($this->servicioReal === null) {
            echo "   [PROXY] Primera llamada real → instanciando servicio...\n";
            $this->servicioReal = new ServicioClimaAPI();
        }

        // 3. Delegar al real
        echo "   ❄️ [PROXY] Cache MISS para '$ciudad' → consultando...\n";
        $temperatura = $this->servicioReal->obtenerTemperatura($ciudad);

        // 4. Guardar en caché
        $this->cache[$ciudad] = [
            'valor' => $temperatura,
            'expira' => time() + $this->ttl,
        ];

        return $temperatura;
    }

    private function enCache(string $ciudad): bool
    {
        return isset($this->cache[$ciudad])
            && $this->cache[$ciudad]['expira'] > time();
    }

    public function limpiarCache(): void
    {
        $this->cache = [];
        echo "   🗑️ [PROXY] Caché limpiada\n";
    }
}

// Uso
$clima = new ProxyClimaConCache(ttl: 60);

echo "--- Primera consulta: Madrid ---\n";
$temp = $clima->obtenerTemperatura('Madrid');
echo "   🌡️ Madrid: {$temp}°C\n";

echo "\n--- Segunda consulta: Madrid (caché) ---\n";
$temp = $clima->obtenerTemperatura('Madrid');
echo "   🌡️ Madrid: {$temp}°C\n";

echo "\n--- Consulta: Barcelona ---\n";
$temp = $clima->obtenerTemperatura('Barcelona');
echo "   🌡️ Barcelona: {$temp}°C\n";

// Salida:
//    [PROXY] Proxy creado (servicio real NO instanciado aún)
// --- Primera consulta: Madrid ---
//    [PROXY] Primera llamada real → instanciando servicio...
//    🌐 [REAL] Inicializando conexión con API externa...
//    ❄️ [PROXY] Cache MISS para 'Madrid' → consultando...
//    🌐 [REAL] Consultando API para 'Madrid'...
//    🌡️ Madrid: 22.5°C
//
// --- Segunda consulta: Madrid (caché) ---
//    ⚡ [PROXY] Cache HIT para 'Madrid'
//    🌡️ Madrid: 22.5°C
//
// --- Consulta: Barcelona ---
//    ❄️ [PROXY] Cache MISS para 'Barcelona' → consultando...
//    🌐 [REAL] Consultando API para 'Barcelona'...
//    🌡️ Barcelona: 18.3°C

Observa los tres niveles de optimización:

  1. Lazy loading: El ServicioClimaAPI no se instancia al crear el proxy. Solo cuando la primera consulta no está en caché.
  2. Caché con TTL: La segunda consulta a Madrid es instantánea, no toca la API.
  3. Transparencia: El cliente solo ve ServicioClima. Le da igual si detrás hay un proxy, una API o un hamster con un termómetro.

🌍 6. Casos de uso en el mundo real

Doctrine Lazy Loading (PHP)

Cuando haces $user->getPosts(), Doctrine no carga los posts de la BD hasta ese momento. El objeto que tienes es un Proxy Virtual generado automáticamente que extiende tu entidad.

Nginx / Reverse Proxy

Nginx se pone delante de tu servidor de aplicación. Gestiona SSL, caché de estáticos, rate limiting, balanceo de carga... El cliente solo ve Nginx. Proxy de infraestructura puro.

JavaScript Proxy (ES6)

JavaScript tiene un objeto Proxy nativo. Con new Proxy(target, handler) puedes interceptar get, set, has, delete... sobre cualquier objeto. Es la base de la reactividad de Vue 3.

API Gateway (Kong, AWS)

Un API Gateway es un proxy que se sienta delante de tus microservicios. Maneja autenticación, rate limiting, transformación de requests, logging. Los microservicios no saben quién les llama.

¿Qué ejemplo del mundo real usa Proxy Virtual para lazy loading de entidades?


🔄 7. Comparativa con otros patrones

PatrónPropósitoDiferencia clave
ProxyControlar el acceso al objeto realMisma interfaz que el Real. El cliente no sabe que habla con un proxy
DecoratorAñadir funcionalidad extraTambién envuelve al objeto, pero su objetivo es añadir comportamiento, no controlar acceso. Se pueden apilar N decoradores
AdapterConvertir una interfaz en otraAdapter cambia la interfaz. Proxy mantiene la misma interfaz. Adapter traduce, Proxy controla
FacadeSimplificar un subsistema complejoFacade ofrece una interfaz nueva y simplificada. Proxy mantiene la misma interfaz del original
FlyweightCompartir estado para ahorrar memoriaFlyweight comparte objetos. Proxy Virtual retrasa su creación. Ambos optimizan pero de forma distinta

La confusión más frecuente es Proxy vs Decorator. Ambos envuelven al objeto real, pero la intención es diferente. El Proxy controla si se accede al real (permisos, caché, lazy loading). El Decorator asume que siempre se accede y le añade funcionalidad (logging decorativo, compresión, cifrado). Además, el Proxy suele gestionar el ciclo de vida del Real (lo crea él), mientras que el Decorator recibe el objeto ya creado.

¿Qué diferencia hay entre Proxy y Decorator?


⚖️ 8. Relación con SOLID

SRP (Single Responsibility)

El Sujeto Real se encarga de su lógica de negocio. El Proxy se encarga del control (acceso, caché, logging). Cada clase, una responsabilidad.

OCP (Open/Closed)

Puedes añadir un proxy sin modificar el Sujeto Real ni el cliente. Nuevo comportamiento (caché, log) sin tocar código existente.

DIP (Dependency Inversion)

El cliente depende de la interfaz ServicioDeDescarga, no de LibreriaDeDescargasPesadas ni del Proxy concreto. Inyectas lo que quieras.

LSP (Liskov Substitution)

El Proxy es un sustituto perfecto del Real porque implementa la misma interfaz. Puedes usar uno u otro sin romper nada. Liskov en estado puro.

¿Qué principio SOLID se cumple porque el Proxy puede sustituir al Real sin romper nada?


✅ 9. Ventajas y desventajas

Ventajas
  • Control de acceso: Permisos, autenticación, rate limiting... sin tocar el Real.
  • Lazy loading: Objetos pesados solo se instancian cuando realmente hacen falta.
  • Caché transparente: Evita operaciones costosas repetidas. El cliente ni se entera.
  • Logging/auditoría: Registra llamadas sin ensuciar la lógica de negocio.
  • Transparencia: El cliente usa la misma interfaz. Puedes introducir un proxy sin cambiar una línea del cliente.
Desventajas
  • Complejidad extra: Una clase más que mantener. Si el Real es simple, es overkill.
  • Latencia adicional: El proxy añade una capa de indirección. En hot paths puede sumar milisegundos.
  • Duplicación de interfaz: Si el Real tiene 20 métodos, el proxy tiene que implementar los 20 (aunque delegue directamente).
  • Debug más difícil: El stack trace pasa por el proxy. Si algo falla, tienes una capa más que inspeccionar.

⚠️ 10. Errores comunes

1. El Proxy God Object: demasiada lógica

Si tu proxy hace validación, caché, logging, transformación de datos y encima envía emails, ya no es un proxy, es un dinosaurio:

// ❌ MAL: el proxy hace de todo
class ProxyTodoEnUno implements ServicioDeDescarga
{
    public function descargar(string $archivo, string $usuario): bool
    {
        $this->verificarPermisos($usuario);    // Protección
        $this->registrarAcceso($usuario);       // Logging
        $cached = $this->buscarEnCache($archivo); // Caché
        $this->enviarNotificacion($usuario);    // ¿Esto qué pinta aquí?
        $this->actualizarEstadisticas();        // ...y esto
        return $this->servicioReal->descargar($archivo, $usuario);
    }
}

// ✅ BIEN: cada proxy, una responsabilidad. Se pueden componer
$servicio = new LibreriaDeDescargasPesadas();
$conLog = new ProxyLogging($servicio);       // Solo logging
$conCache = new ProxyCache($conLog);         // Solo caché
$conAuth = new ProxyProteccion($conCache);   // Solo permisos
// El cliente usa $conAuth, que delega en $conCache, que delega en $conLog...

Composición de proxies: cada uno una cosa, y se encadenan como capas de cebolla (parecido a Decorator).

Si un Proxy acumula validación, caché, logging y notificaciones, ¿qué problema tiene?

2. Proxy que no implementa toda la interfaz

Si el Sujeto tiene 5 métodos y tu Proxy solo implementa 2, estás rompiendo el contrato. El cliente llamará a un método que no existe y todo explota:

// ❌ MAL: el proxy solo implementa descargar(), pero el Real también tiene listar()
interface ServicioDeDescarga
{
    public function descargar(string $archivo, string $usuario): bool;
    public function listar(string $usuario): array;
}

class ProxyIncompleto implements ServicioDeDescarga
{
    public function descargar(string $archivo, string $usuario): bool
    {
        // ... implementado
    }

    // ¡listar() no está! PHP dará error fatal al instanciar
}

// ✅ BIEN: implementa todos los métodos, aunque algunos solo deleguen
class ProxyCompleto implements ServicioDeDescarga
{
    public function descargar(string $archivo, string $usuario): bool
    {
        // Lógica del proxy + delegación
        return $this->servicioReal->descargar($archivo, $usuario);
    }

    public function listar(string $usuario): array
    {
        // Si no necesitas lógica extra, simplemente delega
        return $this->servicioReal->listar($usuario);
    }
}

3. Caché sin invalidación ni TTL

Si tu proxy cachea resultados pero nunca los invalida, devolverá datos obsoletos para siempre:

// ❌ MAL: caché infinita, datos obsoletos
class ProxyCacheEterna implements ServicioClima
{
    private array $cache = [];

    public function obtenerTemperatura(string $ciudad): float
    {
        if (isset($this->cache[$ciudad])) {
            return $this->cache[$ciudad]; // Puede devolver datos de hace 3 días 💀
        }
        $temp = $this->servicioReal->obtenerTemperatura($ciudad);
        $this->cache[$ciudad] = $temp;
        return $temp;
    }
}

// ✅ BIEN: caché con TTL y opción de limpiar
class ProxyCacheConTTL implements ServicioClima
{
    private array $cache = [];
    private int $ttl;

    public function obtenerTemperatura(string $ciudad): float
    {
        if ($this->enCache($ciudad)) {
            return $this->cache[$ciudad]['valor'];
        }
        $temp = $this->servicioReal->obtenerTemperatura($ciudad);
        $this->cache[$ciudad] = [
            'valor' => $temp,
            'expira' => time() + $this->ttl,
        ];
        return $temp;
    }

    private function enCache(string $ciudad): bool
    {
        return isset($this->cache[$ciudad])
            && $this->cache[$ciudad]['expira'] > time();
    }
}

¿Cuál es el mayor riesgo de un Proxy de Caché sin TTL ni invalidación?

4. Crear el Sujeto Real en el constructor (mata el lazy loading)

Si instancias el Real en el constructor del Proxy, pierdes todo el beneficio del Virtual Proxy:

// ❌ MAL: el Real se crea siempre, aunque no se use
class ProxyNoTanLazy implements ServicioClima
{
    private ServicioClimaAPI $real;

    public function __construct()
    {
        $this->real = new ServicioClimaAPI(); // Conexión pesada YA
    }
}

// ✅ BIEN: el Real se crea solo cuando se necesita
class ProxyLazy implements ServicioClima
{
    private ?ServicioClimaAPI $real = null;

    public function obtenerTemperatura(string $ciudad): float
    {
        if ($this->real === null) {
            $this->real = new ServicioClimaAPI(); // Solo aquí
        }
        return $this->real->obtenerTemperatura($ciudad);
    }
}

🤔 11. ¿Cuándo usarlo y cuándo no?

Úsalo cuando...
  • Necesitas control de acceso sin modificar el objeto original
  • El objeto real es costoso de crear y quieres lazy loading
  • Quieres cachear resultados de operaciones caras sin ensuciar la clase real
  • Necesitas logging/auditoría transparente de las llamadas
  • El objeto real está en otro servidor (Proxy Remoto / RPC stubs)
Evítalo cuando...
  • El objeto real es simple y barato de crear (overkill)
  • Puedes añadir la lógica directamente al objeto real (si tienes el código fuente)
  • El objeto real tiene muchos métodos y el proxy solo necesita interceptar 1 (mejor un Decorator específico)
  • Solo quieres cambiar la interfaz (eso es Adapter, no Proxy)

Si te ves escribiendo if ($tienePermisos) { ... } dentro del objeto real, para. Esa lógica pertenece a un Proxy de Protección, no a la clase de negocio.

¿Cuándo NO deberías usar el Patrón Proxy?


💡 12. Conclusión

El Patrón Proxy es tu bestie cuando necesitas interponer una capa de lógica entre un cliente y un objeto sin que el cliente se entere. Control de acceso, lazy loading, caché, logging… todo eso cabe en un Proxy que implementa la misma interfaz que el Real.

A los Proxies los ves por todas partes. Doctrine genera proxies para lazy loading de entidades, Nginx actúa como reverse proxy, JavaScript tiene un Proxy nativo para interceptar operaciones, los API Gateways son proxies de infraestructura. Es un patrón tan natural que muchas veces lo usas sin darte cuenta.

Eso sí, no te pases. Si el objeto real es simple y barato, meter un proxy es matar moscas a cañonazos. Y si tu proxy empieza a acumular lógica, considera partirlo en varios proxies encadenados (uno por responsabilidad) o evalúa si lo que necesitas es realmente un Decorator.

EA, ¡saluditos y nos vemos en los bares! 🍻