Provide a surrogate or placeholder for another object to control access to it.
Proxy provides a substitute for another object and controls access to it. The proxy implements the same interface as the real subject, so clients can't tell the difference. The proxy can do work before or after forwarding the request — lazy loading, caching, access control, logging, and more.
Four major proxy flavours exist: Virtual (lazy init), Protection (access control), Remote (network transparency), and Caching (result memoisation).
You have a heavyweight service object that loads a large image from disk. Most of the time it isn't even used — but it's always initialised at startup, wasting memory and time.
A Virtual Proxy can stand in for the real object, delaying its creation until the first actual use. The client never changes — it just calls methods on what it thinks is the real object, but it's actually calling the lightweight proxy.
// Subject interface public interface Image { void display(); } // RealSubject — expensive to create public class RealImage implements Image { private final String filename; public RealImage(String filename) { this.filename = filename; loadFromDisk(); // expensive — disk I/O } private void loadFromDisk() { System.out.println("Loading " + filename + " from disk..."); } public void display() { System.out.println("Displaying " + filename); } } // ── VIRTUAL PROXY — lazy initialisation ───────────────────────────── public class LazyImageProxy implements Image { private RealImage realImage; // null until first use private final String filename; public LazyImageProxy(String filename) { this.filename = filename; // cheap — no disk load yet } public void display() { if (realImage == null) { realImage = new RealImage(filename); // load only on first call } realImage.display(); // delegate } } // ── PROTECTION PROXY — access control ─────────────────────────────── public class ProtectedImageProxy implements Image { private final RealImage realImage; private final String userRole; public ProtectedImageProxy(String filename, String role) { this.realImage = new RealImage(filename); this.userRole = role; } public void display() { if (!userRole.equals("ADMIN") && !userRole.equals("USER")) { throw new SecurityException("Access denied for role: " + userRole); } System.out.println("[ACCESS LOG] user=" + userRole); realImage.display(); } } // ── CACHING PROXY — memoisation ────────────────────────────────────── public interface DataService { String fetchData(String query); } public class CachingDataProxy implements DataService { private final DataService realService; private final Map<String, String> cache = new HashMap<>(); public CachingDataProxy(DataService real) { this.realService = real; } public String fetchData(String query) { if (cache.containsKey(query)) { System.out.println("[CACHE HIT] " + query); return cache.get(query); } String result = realService.fetchData(query); // expensive DB call cache.put(query, result); return result; } } // ── Dynamic Proxy (Java built-in) ──────────────────────────────────── public class LoggingHandler implements InvocationHandler { private final Object target; public LoggingHandler(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("[LOG] Calling: " + method.getName()); Object result = method.invoke(target, args); System.out.println("[LOG] Done: " + method.getName()); return result; } } // Client creates a JDK dynamic proxy Image proxy = (Image) Proxy.newProxyInstance( Image.class.getClassLoader(), new Class[]{ Image.class }, new LoggingHandler(new RealImage("photo.jpg")) ); proxy.display(); // logs before and after
@Transactional, @Cacheable, @Secured use JDK/CGLIB dynamic proxiesgetOrders() on a User returns a proxy list, DB query fires only on iterationjava.lang.reflect.Proxy — JDK's built-in dynamic proxy for interface-based proxying@Cacheable — wraps methods in a Caching Proxy automaticallyrealSubject == null simultaneously and both create a new RealSubject — expensive double initialisationthis — the call goes directly to the real object, skipping the Spring proxy and its @Transactional/@Cacheable interceptorssynchronized with double-checked locking or an AtomicReference with compareAndSet for safe lazy initialisationjava.lang.reflect.Proxy) if the interface is large, to avoid gaps@Autowired private MyService self) and call self.method(), or use AopContext.currentProxy()// ❌ BREAKING — race condition in Virtual Proxy public void display() { if (realImage == null) { // Thread A and B both see null realImage = new RealImage(file); // both create — double init! } realImage.display(); } // ✅ FIX — double-checked locking with volatile private volatile RealImage realImage; public void display() { if (realImage == null) { synchronized (this) { if (realImage == null) { // second check inside lock realImage = new RealImage(filename); } } } realImage.display(); } // ❌ BREAKING — stale cache, no invalidation public String fetchData(String q) { if (cache.containsKey(q)) return cache.get(q); // never expires! String r = real.fetchData(q); cache.put(q, r); return r; } // ✅ FIX — TTL-based cache entry record CacheEntry(String value, long expiresAt) {} private final Map<String, CacheEntry> cache = new HashMap<>(); private static final long TTL_MS = 60_000; public String fetchData(String q) { CacheEntry entry = cache.get(q); if (entry != null && System.currentTimeMillis() < entry.expiresAt()) { return entry.value(); // ✅ only serve if not expired } String r = real.fetchData(q); cache.put(q, new CacheEntry(r, System.currentTimeMillis() + TTL_MS)); return r; } // ❌ BREAKING — Spring self-invocation bypasses proxy public class OrderService { public void placeOrder() { processPayment(); // ❌ calls this.processPayment() — bypasses @Transactional proxy! } @Transactional public void processPayment() { /* transaction never starts */ } } // ✅ FIX — call through the proxy public class OrderService { @Autowired private OrderService self; // inject own proxy public void placeOrder() { self.processPayment(); // ✅ goes through Spring proxy — @Transactional fires } @Transactional public void processPayment() { /* transaction starts correctly */ } }
this.method() inside a bean bypasses the proxy. Fix by injecting self or using AopContext.currentProxy(). Comes up in every Spring interview.