Design Pattern Course
Proxy is a Structural Design Pattern.
Proxy provides a surrogate or placeholder for another object to control access to it. The client interacts with the proxy as if it were the real object — the proxy implements the same interface — but the proxy controls what happens before and/or after the call reaches the real object.
There are several distinct types of Proxy, each serving a different purpose:
- Virtual Proxy: Defers the creation of an expensive object until it is actually needed (lazy initialization). Common for loading large objects from disk, database, or network only when first accessed rather than up-front.
- Protection Proxy: Controls access to the original object based on permissions, authentication, or authorization rules. The example below is a protection proxy.
- Remote Proxy: Provides a local representative for an object that lives in a different address space (different JVM, different machine). The local proxy handles serialization, network communication, and deserialization transparently to the client. Java RMI stubs are the classic example of this.
- Caching Proxy: Caches results of expensive operations and serves subsequent calls from the cache, without involving the real object unless needed.
- Logging/Monitoring Proxy: Adds logging, metrics, or auditing around calls to the real object — common in AOP (Aspect-Oriented Programming) and in Spring's own proxy-based AOP mechanism.
package com.praveen.designpatterns.structural.proxy; public interface Internet { void connectTo(String host) throws Exception; }
package com.praveen.designpatterns.structural.proxy; public class RealInternet implements Internet { @Override public void connectTo(String host) throws Exception { System.out.println("Connecting to " + host); } }
package com.praveen.designpatterns.structural.proxy; import java.util.List; public class InternetProxy implements Internet { private final Internet internet = new RealInternet(); private static final List<String> RESTRICTED_SITES = List.of("gmail.com"); @Override public void connectTo(String host) throws Exception { if (RESTRICTED_SITES.contains(host.toLowerCase())) { throw new Exception("Company policy restricts access to " + host); } internet.connectTo(host); } }
package com.praveen.designpatterns.structural.proxy; public class ProxyPatternTest { public static void main(String[] args) { Internet internet = new InternetProxy(); try { internet.connectTo("praveen-oruganti.firebaseapp.com"); internet.connectTo("gmail.com"); } catch (Exception e) { System.out.println(e.getMessage()); } } }
Output
Connecting to praveen-oruganti.firebaseapp.com
Company policy restricts access to gmail.com
A couple of improvements from the original: restrictedSites is now static final and initialized with List.of() (Java 9+) rather than a mutable ArrayList populated in a static block — a restricted list of this kind should be immutable. The internet field is also final since it's set at construction and never re-assigned.
The most important real-world note: in modern Spring applications, the Proxy pattern is used everywhere under the hood — Spring AOP works by creating dynamic proxies around your beans (either JDK dynamic proxies via java.lang.reflect.Proxy when the bean implements an interface, or CGLIB subclass proxies when it doesn't). This is exactly how @Transactional, @Cacheable, @Async, @Retryable, and security annotations are applied transparently to your service methods. You don't write the proxy yourself, but understanding the Proxy pattern is exactly what explains why calling a @Transactional method on this (bypassing the proxy) doesn't start a transaction, while calling it through an injected reference to the bean (via the proxy) does. That's a concrete, production-relevant consequence of how the pattern works.
When to Use the Proxy Pattern
- Virtual proxy: when you want to delay costly object creation until the object is actually needed.
- Protection proxy: when different callers should have different access rights to the same object.
- Remote proxy: when you need a local stand-in for an object in a remote JVM or service.
- Caching proxy: when the same expensive operation is called repeatedly with the same parameters.
- Logging/monitoring proxy: when you want to add cross-cutting concerns (auditing, metrics) without modifying the real object.
Proxy Pattern Examples in the JDK
java.lang.reflect.Proxy— Java's built-in dynamic proxy API. Lets you create a proxy for any interface at runtime viaProxy.newProxyInstance()with anInvocationHandler. This is the mechanism Spring AOP uses for interface-based proxies.java.rmi.*— the entire RMI package is a remote proxy implementation. RMI stubs are generated proxy objects that make remote method calls look like local ones to the caller.