El patrón Proxy es uno de los patrones estructurales más útiles cuando quieres interponer una capa de control entre un cliente y un objeto real. En términos simples, el proxy actúa como un reemplazo del objeto original, implementa la misma interfaz y decide qué hacer antes o después de delegar la operación. Esto lo vuelve especialmente valioso en software profesional, donde no basta con "llamar a un método": muchas veces también necesitas validar permisos, retrasar cargas costosas, guardar resultados en caché, registrar accesos o incluso coordinar concurrencia.
Si alguna vez cargaste datos solo cuando el usuario los necesitó, protegiste una operación sensible con autorización, evitaste repetir consultas caras o registraste llamadas a un servicio externo, ya estuviste resolviendo un problema típico para Proxy. La ventaja del patrón es que organiza esa lógica sin ensuciar al consumidor ni sobrecargar la clase real con responsabilidades transversales.
¿Qué es el patrón Proxy?
Proxy proporciona un objeto intermediario que expone la misma interfaz que el objeto real y controla el acceso a él. Desde el punto de vista del cliente, ambos deberían poder usarse de la misma manera. La diferencia es que el proxy puede tomar decisiones antes de delegar la llamada: por ejemplo, verificar credenciales, inicializar el objeto real de manera diferida, devolver datos cacheados o registrar la operación.
La idea clave es esta: el cliente depende de una abstracción común, no de la implementación concreta. Gracias a eso, puedes intercambiar el objeto real por su proxy sin romper el contrato del sistema.
¿Cuándo conviene usar Proxy?
- Cuando necesitas lazy loading de datos, archivos, imágenes o conexiones costosas.
- Cuando debes agregar seguridad, autorización o validaciones antes de ejecutar una operación.
- Cuando quieres registrar operaciones para auditoría, métricas o debugging.
- Cuando necesitas cachear respuestas para reducir latencia y carga en servicios externos.
- Cuando debes controlar concurrencia, coordinar acceso a recursos compartidos o agrupar solicitudes.
- Cuando no quieres modificar la clase real porque pertenece a otra capa, librería o contexto más sensible.
Estructura mental del patrón
- Subject: la interfaz común que conocen cliente, proxy y objeto real.
- RealSubject: la implementación real que contiene la lógica principal.
- Proxy: el intermediario que implementa la misma interfaz y controla el acceso al RealSubject.
- Client: consume la interfaz sin depender directamente de si está hablando con el objeto real o con el proxy.
Ejemplo en TypeScript: caché y deduplicación de requests
Imagina un frontend o backend en Node.js que consulta un servicio de productos. Si varios componentes piden el mismo producto casi al mismo tiempo, no quieres golpear la API externa varias veces. Un proxy puede resolver dos problemas a la vez: cachear resultados y agrupar solicitudes concurrentes para la misma clave.
interface ProductService {
getProductById(id: string): Promise<Product>;
}
interface Product {
id: string;
name: string;
price: number;
}
class ApiProductService implements ProductService {
async getProductById(id: string): Promise<Product> {
console.log(`[API] Consultando producto ${id}`);
// Simula latencia de red
await new Promise((resolve) => setTimeout(resolve, 800));
return {
id,
name: `Producto ${id}`,
price: 199.99,
};
}
}
class ProductServiceProxy implements ProductService {
private cache = new Map<string, Product>();
private inFlightRequests = new Map<string, Promise<Product>>();
constructor(private readonly realService: ProductService) {}
async getProductById(id: string): Promise<Product> {
if (this.cache.has(id)) {
console.log(`[Proxy] Retornando ${id} desde caché`);
return this.cache.get(id)!;
}
if (this.inFlightRequests.has(id)) {
console.log(`[Proxy] Reutilizando request en curso para ${id}`);
return this.inFlightRequests.get(id)!;
}
const request = this.realService.getProductById(id)
.then((product) => {
this.cache.set(id, product);
return product;
})
.finally(() => {
this.inFlightRequests.delete(id);
});
this.inFlightRequests.set(id, request);
return request;
}
}
async function main() {
const service: ProductService = new ProductServiceProxy(new ApiProductService());
const [p1, p2, p3] = await Promise.all([
service.getProductById("A-100"),
service.getProductById("A-100"),
service.getProductById("A-100"),
]);
console.log(p1, p2, p3);
const cached = await service.getProductById("A-100");
console.log(cached);
}
main();¿Qué está pasando aquí? El cliente solo conoce ProductService. El proxy revisa primero si el producto ya fue cargado. Si existe en caché, lo devuelve de inmediato. Si todavía no está, pero ya hay una llamada en curso para ese mismo id, en lugar de crear otra petición reutiliza la promesa existente. Ese detalle es muy valioso en sistemas reales con tráfico concurrente.
Este enfoque encaja muy bien en BFFs, APIs, dashboards, e-commerce y apps que consumen servicios costosos o con límites de uso. Además, el código del cliente se mantiene limpio porque no tiene que saber nada sobre caché ni coordinación de requests.
Ejemplo en Java: seguridad y auditoría
Ahora piensa en un sistema empresarial donde ciertos documentos solo pueden ser leídos por usuarios autorizados. Aquí el proxy funciona como una puerta de control: valida permisos, registra intentos de acceso y recién entonces delega al objeto real.
interface DocumentService {
String getDocument(String userRole, String documentId);
}
class RealDocumentService implements DocumentService {
@Override
public String getDocument(String userRole, String documentId) {
return "Contenido sensible del documento " + documentId;
}
}
class SecureDocumentProxy implements DocumentService {
private final DocumentService realService;
public SecureDocumentProxy(DocumentService realService) {
this.realService = realService;
}
@Override
public String getDocument(String userRole, String documentId) {
logAccess(userRole, documentId);
if (!hasPermission(userRole)) {
throw new SecurityException("Acceso denegado para el rol: " + userRole);
}
return realService.getDocument(userRole, documentId);
}
private boolean hasPermission(String userRole) {
return "ADMIN".equals(userRole) || "AUDITOR".equals(userRole);
}
private void logAccess(String userRole, String documentId) {
System.out.println("[AUDIT] Rol=" + userRole + " intenta acceder al documento " + documentId);
}
}
public class Main {
public static void main(String[] args) {
DocumentService service = new SecureDocumentProxy(new RealDocumentService());
try {
System.out.println(service.getDocument("ADMIN", "DOC-9001"));
System.out.println(service.getDocument("GUEST", "DOC-9001"));
} catch (SecurityException ex) {
System.out.println(ex.getMessage());
}
}
}Este ejemplo es típico en backends corporativos, ERPs, plataformas bancarias, sistemas médicos o aplicaciones internas con reglas de acceso. Observa el beneficio arquitectónico: la clase real se concentra en entregar el documento, mientras el proxy encapsula políticas de seguridad y trazabilidad.
Cómo identificar si necesitas Proxy
- Detecta una dependencia costosa, sensible o compartida.
- Verifica si el cliente debería seguir usando la misma interfaz sin enterarse del control adicional.
- Separa qué parte es lógica principal y qué parte es control de acceso, carga, caché o registro.
- Crea una interfaz común para el objeto real y el proxy.
- Haz que el proxy delegue solo cuando las condiciones necesarias se cumplan.
Ventajas del patrón Proxy
- Reduce acoplamiento entre cliente y detalles operativos.
- Permite agregar políticas transversales sin tocar al objeto real.
- Mejora rendimiento mediante caché o lazy loading.
- Facilita auditoría, seguridad y monitoreo.
- Ayuda a centralizar reglas de acceso y control.
Riesgos y errores comunes
- Convertir el proxy en una clase gigantesca con demasiadas responsabilidades.
- Ocultar latencia o efectos secundarios y sorprender al cliente.
- Implementar caché sin estrategia de expiración o invalidación.
- Agregar demasiadas capas de proxy y volver difícil el debugging.
- Confundir Proxy con Decorator: ambos envuelven objetos, pero Proxy suele enfocarse en control de acceso y no solo en extender comportamiento.
Preguntas frecuentes sobre Proxy
¿Proxy y Decorator son lo mismo?
No. Se parecen porque ambos envuelven un objeto y comparten interfaz, pero su intención principal cambia. Decorator suele agregar responsabilidades o comportamientos de forma componible. Proxy, en cambio, se enfoca en controlar el acceso: seguridad, carga diferida, caché, acceso remoto o coordinación.
¿El cliente sabe que está usando un proxy?
Idealmente, no debería importarle. El cliente trabaja contra una interfaz común. Puede saberlo a nivel de arquitectura, pero no necesita cambiar su código para usar el proxy en lugar del objeto real.
¿Cuándo no conviene usar Proxy?
No conviene cuando la lógica adicional es mínima, no hay costo real en acceder al objeto y la capa extra solo complica el diseño. Tampoco ayuda si el problema se resuelve mejor con middleware, interceptores o decoradores ya provistos por tu framework.
¿Puede un proxy mejorar el rendimiento?
Sí, especialmente con lazy loading, memoización, caché y deduplicación de solicitudes. Pero ese beneficio depende de una buena estrategia de consistencia y de que el costo evitado sea realmente significativo.
Idea final
El patrón Proxy no solo "pone una capa en el medio": te da una manera elegante de gobernar el acceso a objetos importantes sin romper la experiencia del cliente. Cuando lo aplicas bien, mejoras diseño, rendimiento y seguridad al mismo tiempo. De los patrones estructurales, Proxy es uno de los más cercanos a problemas reales del día a día: llamadas remotas, recursos costosos, permisos, logs, caché y concurrencia. Por eso vale la pena dominarlo más allá de la definición académica.