Proxy Pattern
Control access to an object by placing a surrogate in front of it. An intermediate structural pattern — the gatekeeper behind lazy loading, access control, caching, logging, and every remote-method invocation in Java.
Why Proxy Exists
You have a DatabaseConnection that opens a real connection to a remote database. The problem: opening that connection takes 3 seconds and allocates 50 MB of memory. But half the time, the caller just checks isConnected() or reads a cached result — the real connection is never actually needed. You also want to check permissions before any query runs, log every call for auditing, and cache repeated queries. The naive approach: cram all of this into the DatabaseConnection class itself.
What goes wrong:
- SRP violation — one class handles five responsibilities: real database work, access control, logging, caching, and lazy initialisation. Change one concern โ risk breaking them all.
- Eager resource allocation — the real connection is created even when it’s not needed. Half the callers only need cached results, but they pay the 3-second startup cost.
- Untestable — testing the query logic requires mocking auth, logger, cache, and the real connection simultaneously. Five mocks per test.
- Not reusable — another service needs the same
DatabaseConnectionbut without access control (internal batch job). You can’t get the connection without the security check. - Cross-cutting logic duplicated — if you add a second service (
FileStorage), you copy-paste the same auth/logging/caching code into it.
This is the problem Proxy solves. Instead of bloating the real object, you place a surrogate (proxy) in front of it that implements the same interface. The proxy handles access control, logging, caching, or lazy initialisation — and delegates to the real object only when necessary. The client doesn’t know (or care) whether it’s talking to the proxy or the real thing.
What Is Proxy?
Proxy is a structural pattern that provides a surrogate or placeholder for another object to control access to it. The proxy implements the same interface as the real object, so the client can’t tell the difference. Before (or after) forwarding the call to the real object, the proxy adds its own behaviour: lazy loading, access checks, logging, caching, or remote communication.
— Gamma, Helm, Johnson, Vlissides (1994)
The four types of proxy:
| Proxy Type | What It Controls | Real-World Example |
|---|---|---|
| Virtual Proxy (lazy) | Defers expensive object creation until the object is actually used. Holds a placeholder; creates the real object on first call. | An image viewer that loads a 20 MB photo only when you scroll to it, not when the gallery opens. |
| Protection Proxy (security) | Controls access based on permissions. Checks credentials before forwarding the call to the real object. | A database proxy that checks user.hasPermission("DB_READ") before executing any query. |
| Caching Proxy | Stores results of expensive operations and returns cached results for identical requests. | An HTTP proxy that caches GET responses so identical requests don’t hit the origin server. |
| Remote Proxy | Represents an object in a different address space (another JVM, another server). Handles network communication transparently. | Java RMI stubs — the client calls service.getData() locally, the proxy serialises the call and sends it over the network. |
- Both wrap an object behind the same interface. The difference is intent.
- Decorator adds new behaviour to make the object do more (e.g. add compression to a stream).
- Proxy controls access to the object — it may delay creation, check permissions, cache results, or represent a remote object.
- Decorator enriches; Proxy guards.
- If the wrapper can prevent the call from reaching the real object entirely (deny access, return cached result), it’s a Proxy.
- If it always forwards and just adds extra work, it’s a Decorator.
Participants & Structure
| Participant | Role | In the Database Example |
|---|---|---|
| Subject (interface) | The common interface that both the RealSubject and Proxy implement. The client programs to this interface exclusively — it never knows whether it holds a proxy or the real object. | DataService — declares query(sql) : ResultSet. Both the real connection and the proxy implement this. |
| RealSubject | The actual object that does the real work. Contains the core business logic. Has no knowledge of any proxies wrapping it. Expensive to create and/or access. | RealDatabaseConnection — opens a real JDBC connection, executes queries. Costs 3s to initialise, 50 MB of RAM. |
| Proxy | Implements the same Subject interface. Holds a reference to the RealSubject (or creates it lazily). Adds control logic (auth, logging, caching, lazy init) before/after delegating to the RealSubject. May short-circuit the call entirely (deny access, return cached result). | DatabaseProxy — checks permissions, logs the SQL, checks the cache. Only creates and calls the real connection if the cache misses. |
| Client | Works with the Subject interface only. Doesn’t know (or care) whether it’s talking to the proxy or the real object. Receives the proxy via dependency injection or a factory. | The REST controller injects a DataService — at runtime, it’s a proxy that wraps the real connection. |
- The proxy must implement the same interface as the real object.
- This is what makes it transparent to the client.
- If the proxy had a different interface, the client would need to know about the proxy — and you’d lose the ability to swap proxy โ real object at configuration time.
- In Spring, this is how AOP works: Spring generates a proxy that implements your service interface and injects it where the real bean was expected.
The Pattern In Motion
Scenario: A client calls query("SELECT * FROM orders") on what it thinks is a DataService. In reality, it’s a proxy that guards the real connection with lazy init, access control, and caching.
service.query(sql)DataService reference. It doesn’t know that service is a DatabaseProxy, not the real connection. The call enters the proxy.user.hasPermission("DB_READ"). If denied โ throw SecurityException. The real connection is never touched. Short-circuit: call blocked.sql in its internal Map. If the result exists โ return it immediately. The real connection is never touched. Short-circuit: cached result returned.realConnection == null. If so, it creates the real RealDatabaseConnection now (3 seconds). This is deferred until the first actual need — not at startup.real.query(sql)ResultSet. The proxy caches the result for future calls, then returns it to the client.- Notice how the proxy can return without ever calling the real object — if permission is denied (Step 2) or the cache hits (Step 3).
- This is the fundamental difference from Decorator: a Decorator always forwards to the wrapped object; a Proxy can intercept and block.
- The real
RealDatabaseConnectionmay never be created if all queries are cached or denied.
You Already Use This
Proxy is one of the most heavily used patterns in Java — from the JDK itself to Spring, JPA, and every RMI call.
java.lang.reflect.Proxy — Java’s built-in dynamic proxy. Given an interface and an InvocationHandler, it generates a proxy class at runtime that implements the interface and routes every method call through your handler. This is the foundation of Spring AOP, Mockito, and most Java mocking frameworks. You don’t write a proxy class — the JVM writes it for you. Spring AOP / @Transactional — when you annotate a method with @Transactional, Spring generates a proxy around your bean. The proxy starts a transaction before the method, commits on success, and rolls back on exception — all without touching your code. The client injects your service interface; at runtime, it gets the proxy. Same interface, controlled access, transparent to the caller. Pure Proxy pattern. JPA / Hibernate lazy loading — when you call order.getCustomer(), Hibernate may return a proxy object instead of loading the customer from the database. The proxy implements the Customer interface but delays the SQL query until you actually access a field (proxy.getName()). This is a virtual proxy: expensive creation (database query) is deferred until first use. java.rmi (Remote Method Invocation) — the client holds a stub that implements the remote service interface. Calling stub.getData() looks like a local call, but the stub serialises the method name and arguments, sends them over the network to the server, deserialises the response, and returns it. A textbook remote proxy — a local representative for a remote object. LazyInitializationException: - Hibernate’s virtual proxy for lazy-loaded entities defers the database query until you access a field.
- But if the Hibernate session is already closed (e.g. you returned the entity from a
@Transactionalmethod and try to access a lazy field in the controller), the proxy can’t fetch the data โLazyInitializationException. - Understanding that these are proxy objects, not real entities, is key to avoiding this common bug.
Build It Once
Domain: Image Viewer. A Image interface with display(). A RealImage that loads from disk (expensive). A ProxyImage that defers loading until first display() call, and an AccessControlProxy that checks user role before displaying.
- In the Protection Proxy demo,
AccessControlProxywraps aProxyImagewhich wraps aRealImage. - Each proxy adds one concern.
- You could add a
LoggingProxybetween them with zero changes to existing classes. - This composability is what makes Proxy so powerful in frameworks like Spring, where AOP stacks multiple proxies (transaction, security, logging) around a single bean.
Common Mistakes
- The classic virtual proxy pattern
if (real == null) real = new RealSubject()has a race condition in multi-threaded code. - Two threads can both see
nulland create two real objects. - Fix: Use
synchronized, double-checked locking, or (best) make the fieldvolatileand use the holder idiom. In Spring, this isn’t an issue because Spring manages the proxy lifecycle.
- Adapter changes the interface (converts A to B).
- Proxy keeps the same interface and controls access.
- If the wrapper changes method signatures or translates data formats, it’s an Adapter.
- If it keeps the exact same interface and adds auth/cache/lazy-init, it’s a Proxy.
- The test: can the client use the wrapper and the real object interchangeably without any code change? If yes โ Proxy.
- If it must change calls โ Adapter.
When To Use Proxy
- Virtual Proxy: Object creation is expensive and may not be needed at all — defer it until first use (JPA lazy loading, large image loading, database connections)
- Protection Proxy: You want to add access control to an existing object without modifying it — check permissions before every method call (RBAC, read-only views)
- Caching Proxy: Results of expensive operations are reused — cache results and short-circuit repeated calls with the same arguments
- Remote Proxy: The real object lives in another JVM, server, or process — use a local stub that handles serialisation and network communication transparently (RMI, gRPC stubs)
- You want to add cross-cutting concerns (logging, metrics, transaction management) to an existing class without modifying it — the basis of Spring AOP
- The real object is cheap to create and always needed — lazy loading adds complexity for nothing
- You need to add business behaviour, not just control access — prefer Decorator (which adds new capabilities) or a service layer
- You need a different interface — that’s Adapter (1:1 translation), not Proxy (same interface, controlled access)
- The overhead of the extra method call and conditional logic measurably impacts performance in a hot code path
| Pattern | Interface? | Purpose | Can block the call? |
|---|---|---|---|
| Proxy ← this | Same as real | Control access (lazy, auth, cache, remote) | Yes — deny, cache-hit, or defer |
| Decorator | Same as wrapped | Add new behaviour / capabilities | No — always delegates |
| Adapter | Different (converts A โ B) | Interface translation | No — always translates + delegates |
| Facade | New simplified interface | Simplify N interfaces into 1 | No — always delegates to subsystem |
Problems To Solve
Proxy problems test whether you can identify which type of proxy is needed, implement the same-interface constraint correctly, and handle short-circuiting (deny, cache-hit, lazy-init) before delegation.
| Difficulty | Problem | Key Insight |
|---|---|---|
| Easy | Virtual Proxy for Large Report A Report interface has generate() : String and getTitle() : String. A RealReport queries a database (2 seconds) in its constructor and generates a 50-page result. Build a ReportProxy that defers the database query until generate() is first called. getTitle() must work immediately without triggering the expensive query. | The simplest proxy: lazy initialisation. Tests that only the expensive operation (generate()) triggers real object creation, while cheap operations (getTitle()) are handled by the proxy directly without loading the real object. |
| Easy | Read-Only Protection Proxy A UserRepository interface has findById(id), findAll(), save(user), and delete(id). Build a ReadOnlyRepositoryProxy that allows findById and findAll to pass through to the real repository, but throws UnsupportedOperationException for save and delete. Use it in a guest user context. | Tests protection proxy for operation-level access control (read vs. write), not just identity-based auth. The proxy short-circuits mutating operations entirely โ the real repository never receives them. Common pattern for read-only database connections and view-only API endpoints. |
| Medium | Memoisation Caching Proxy A WeatherService interface has getForecast(city) : Forecast. The real implementation makes an HTTP call (500ms). Build a CachingWeatherProxy that caches results keyed by city for 10 minutes. If the cached entry exists and is fresh, return it without calling the real service. If stale or absent, call the real service, update the cache, and return. Print cache hit/miss on each call. | Tests caching proxy with TTL (time-to-live) eviction. The proxy must store both the result and the timestamp. Requires checking both existence and freshness before short-circuiting. Also demonstrates how the caching proxy transparently improves performance without the client knowing. |
| Medium | Logging Proxy via Dynamic Proxy Given any interface (start with Calculator having add, subtract, multiply), build a generic logging proxy factory using java.lang.reflect.Proxy that wraps any interface implementation and logs: the method name, the arguments, the return value, and the execution time in milliseconds. The factory should work for any interface, not just Calculator. | Tests java.lang.reflect.Proxy and InvocationHandler — the JVM’s built-in dynamic proxy mechanism. Unlike static proxies (one proxy class per interface), a dynamic proxy handles any interface at runtime. This is the foundation of Spring AOP and Mockito. Tests reflection proficiency and understanding of how frameworks like Spring generate transparent proxies. |
| Hard | Circuit Breaker Proxy A PaymentGateway interface has charge(amount, card) : ChargeResult. The real gateway is an HTTP service that occasionally times out. Build a CircuitBreakerProxy that wraps the gateway with three states: CLOSED (normal, pass through), OPEN (gateway is failing โ fail fast with an exception, don’t call the real service), HALF_OPEN (trial โ allow one call through; if it succeeds, go CLOSED; if it fails, go back to OPEN). Transitions: after 5 consecutive failures, go OPEN; after 30 seconds in OPEN, go HALF_OPEN. Track failure count, success count, and state transitions. | Tests an advanced protection proxy used in production microservices (the Circuit Breaker pattern by Michael Nygard). The proxy must maintain state across calls (failure count, last-failure time) and make real-time decisions about whether to forward calls. The Proxy pattern is the structural backbone of every circuit breaker library (Resilience4j, Hystrix). Tests state management in a proxy and the interaction between same-interface wrapping and operational resilience. |
- When asked about Proxy, the interviewer wants to see: (1) the proxy implements the same interface as the real object; (2) you name the correct proxy type (virtual/protection/caching/remote); (3) the proxy can short-circuit (deny, return cached, defer creation) — unlike Decorator which always forwards; (4) awareness that Spring
@Transactional,@Cacheable, and Hibernate lazy loading are all proxy-based. - Stand-out answers discuss
java.lang.reflect.Proxyfor dynamic proxies and distinguish Proxy (same interface, access control) from Decorator (same interface, added capability) and Adapter (different interface, translation).