Structural

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.

Overview ยท Structural ยท Singleton ยท Factory Method ยท Abstract Factory ยท Builder ยท Prototype ยท Adapter ยท Bridge ยท Composite ยท Decorator ยท Facade ยท Flyweight ยท Proxy ยท Observer ยท Strategy ยท Command ยท Template Method ยท Chain of Resp. ยท State ยท Mediator ยท Iterator ยท Visitor ยท Memento ยท Interpreter
Category: Structural Difficulty: Intermediate Interview: Tier 2 Confused with: Decorator
01
Section One ยท The Problem

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.

Naive approach — cross-cutting concerns inside the real object
// โœ— The real service class is bloated with cross-cutting concerns class DatabaseConnection { private Connection conn; // expensive โ€” 3s to open, 50MB RAM public ResultSet query(String sql, User user) { // โœ— Access control mixed in if (!user.hasPermission("DB_READ")) throw new SecurityException("Access denied"); // โœ— Logging mixed in logger.info("Query by " + user.getName() + ": " + sql); // โœ— Caching mixed in if (cache.contains(sql)) return cache.get(sql); // โœ— Lazy init mixed in if (conn == null) conn = openConnection(); // 3 seconds ResultSet rs = conn.executeQuery(sql); cache.put(sql, rs); return rs; } } // โœ— SRP violated โ€” one class handles auth, logging, caching, lazy init, AND queries // โœ— Can't reuse DatabaseConnection without dragging in all concerns // โœ— Can't test the query logic without mocking auth, logging, cache

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 DatabaseConnection but 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.
Without Proxy — concerns tangled in the real object
Client DatabaseConnection (bloated) auth check logging caching lazy init real query 5 responsibilities in 1 class ✗ SRP violated — auth, logging, caching, lazy init, query all in one class ✗ Can’t separate concerns — batch job needs queries without auth, can’t get it ✗ Eager allocation — 3s + 50MB even when the cached result was sufficient

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.

02
Section Two ยท The Pattern

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.

GoF Intent: “Provide a surrogate or placeholder for another object to control access to it.”
— Gamma, Helm, Johnson, Vlissides (1994)
Analogy — corporate receptionist: You want to meet the CEO. You don’t walk straight into the CEO’s office — you go through the receptionist. The receptionist implements the same “schedule a meeting” interface the CEO does. But before forwarding your request, the receptionist checks your ID (access control), logs your visit (auditing), and may handle simple requests herself without bothering the CEO (caching). If you pass all checks, the receptionist forwards your request to the CEO (delegation). You never interact with the CEO directly; you interact with the receptionist, who controls access to the CEO. That’s a proxy.

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.
The Proxy implements the same interface as the real object (the Subject)
The client is unaware it’s talking to a proxy. You can swap proxy โ†” real object without changing client code.
The Proxy holds a reference to the real object (or creates it lazily)
Delegation: the proxy forwards calls to the real object after performing its own logic (auth, log, cache).
The Proxy adds cross-cutting behaviour before/after delegation
The real object stays clean — it only does its core job. Auth, logging, caching live in the proxy. SRP preserved.
Proxies can be stacked: LoggingProxy โ†’ CachingProxy โ†’ ProtectionProxy โ†’ RealService
Each proxy adds one concern. The chain is composable and each layer is independently testable.
With Proxy — concerns separated, real object stays clean
Client Protection Proxy auth check Logging Proxy audit log Caching Proxy cache results DatabaseConnection only: execute query ✔ Each proxy has one responsibility — SRP preserved ✔ Real object is clean — just database queries, no auth/log/cache ✔ Proxies are composable — add/remove layers without touching others ✔ Client sees only the interface — doesn’t know proxies exist
Proxy vs. Decorator:
  • 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.
03
Section Three ยท Anatomy

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.
Proxy — UML Class Diagram
«interface» DataService + query(sql) : ResultSet RealDatabaseConnection - connection : Connection + query(sql) : ResultSet DatabaseProxy - real : DataService - cache : Map + query(sql) : ResultSet delegates to Client uses interface ■ Blue = Subject interface ■ Gold = Proxy (adds control logic) ■ Green = RealSubject (does the actual work) --▷ dashed = implements / depends on Proxy and RealSubject implement the same interface Client sees only DataService โ€” doesn’t know which impl it has
Same interface is the key constraint:
  • 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.
04
Section Four ยท How It Works

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.

Step 1 — Client calls service.query(sql)
The client has a DataService reference. It doesn’t know that service is a DatabaseProxy, not the real connection. The call enters the proxy.
Step 2 — Proxy checks permissions (Protection Proxy)
The proxy calls user.hasPermission("DB_READ"). If denied โ†’ throw SecurityException. The real connection is never touched. Short-circuit: call blocked.
Step 3 — Proxy checks cache (Caching Proxy)
The proxy looks up sql in its internal Map. If the result exists โ†’ return it immediately. The real connection is never touched. Short-circuit: cached result returned.
Step 4 — Proxy creates real connection lazily (Virtual Proxy)
Cache miss. The proxy checks if realConnection == null. If so, it creates the real RealDatabaseConnection now (3 seconds). This is deferred until the first actual need — not at startup.
Step 5 — Proxy delegates to real object: real.query(sql)
The real connection executes the SQL, returns a ResultSet. The proxy caches the result for future calls, then returns it to the client.
Step 6 — Same query called again
Proxy hits the cache at Step 3 โ†’ returns instantly. The real connection is not called. The 3-second query becomes a nanosecond cache lookup.
Proxy — Request Flow (query call)
Client DatabaseProxy RealDatabaseConnection query(sql) auth check cache lookup MISS lazy create real query(sql) ResultSet cache.put(sql,rs) ResultSet query(sql) again cache HIT cached ResultSet (instant)
The pattern in pseudocode
// โ”€โ”€ Subject interface โ”€โ”€ interface DataService { ResultSet query(String sql); } // โ”€โ”€ RealSubject โ”€โ”€ class RealDatabaseConnection implements DataService { public ResultSet query(String sql) { return connection.executeQuery(sql); // real work } } // โ”€โ”€ Proxy (combines virtual + protection + caching) โ”€โ”€ class DatabaseProxy implements DataService { private DataService real; // null until first use (lazy) private final Map<String, ResultSet> cache = new HashMap<>(); public ResultSet query(String sql) { // 1. Protection: check access if (!SecurityContext.hasPermission("DB_READ")) throw new SecurityException("Denied"); // 2. Caching: return cached result if available if (cache.containsKey(sql)) return cache.get(sql); // 3. Virtual: create real object lazily if (real == null) real = new RealDatabaseConnection(); // 4. Delegate to real object ResultSet rs = real.query(sql); cache.put(sql, rs); return rs; } } // โ”€โ”€ Client โ”€โ”€ DataService service = new DatabaseProxy(); // client sees interface only ResultSet rs = service.query("SELECT * FROM orders"); // โœ“ Auth checked, cache consulted, real connection created only if needed
The proxy can short-circuit:
  • 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 RealDatabaseConnection may never be created if all queries are cached or denied.
05
Section Five ยท Java Stdlib

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.

IN JAVA
Example 1 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.
Example 2 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.
Example 3 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.
Example 4 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.
Stdlib usage — java.lang.reflect.Proxy (dynamic proxy)
// โ”€โ”€ Define the Subject interface โ”€โ”€ interface Greeter { String greet(String name); } // โ”€โ”€ Real implementation โ”€โ”€ class RealGreeter implements Greeter { public String greet(String name) { return "Hello, " + name + "!"; } } // โ”€โ”€ Create a dynamic proxy at runtime โ”€โ”€ Greeter real = new RealGreeter(); Greeter proxy = (Greeter) Proxy.newProxyInstance( Greeter.class.getClassLoader(), new Class[]{ Greeter.class }, (Object p, Method method, Object[] args) -> { System.out.println("[LOG] Calling " + method.getName()); // proxy logic Object result = method.invoke(real, args); // delegate System.out.println("[LOG] Result: " + result); // proxy logic return result; } ); proxy.greet("Alice"); // [LOG] Calling greet // [LOG] Result: Hello, Alice!
Stdlib usage — Spring @Transactional as Proxy
// Your code โ€” no proxy logic here @Service public class OrderService { @Transactional // โ† Spring generates a proxy around this method public void placeOrder(Order order) { orderRepo.save(order); paymentService.charge(order); // if exception โ†’ proxy rolls back transaction // if success โ†’ proxy commits transaction } } // What Spring injects into the controller: // NOT the real OrderService โ€” a PROXY that wraps it // Proxy: begin tx โ†’ call real.placeOrder() โ†’ commit/rollback
Hibernate proxies cause the famous 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 @Transactional method 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.
06
Section Six ยท Implementation

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.

Java — Proxy Pattern Image Viewer (core)
// โ”€โ”€ Subject interface โ”€โ”€ interface Image { void display(); String getFilename(); } // โ”€โ”€ RealSubject (expensive to create) โ”€โ”€ class RealImage implements Image { private final String filename; public RealImage(String filename) { this.filename = filename; loadFromDisk(); // expensive! 2 seconds } private void loadFromDisk() { System.out.println(" โณ Loading " + filename + " from disk..."); } public void display() { System.out.println(" ๐Ÿ–ผ๏ธ Displaying " + filename); } public String getFilename() { return filename; } } // โ”€โ”€ Virtual Proxy (lazy loading) โ”€โ”€ class ProxyImage implements Image { private final String filename; private RealImage real; // null until first display() public ProxyImage(String filename) { this.filename = filename; } public void display() { if (real == null) real = new RealImage(filename); // lazy! real.display(); } public String getFilename() { return filename; } // no loading needed }
Proxies compose naturally:
  • In the Protection Proxy demo, AccessControlProxy wraps a ProxyImage which wraps a RealImage.
  • Each proxy adds one concern.
  • You could add a LoggingProxy between 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.
07
Section Seven ยท Watch Out

Common Mistakes

Mistake #1 — Proxy with a different interface than the real object: The entire point of Proxy is transparency — the client doesn’t know it’s talking to a proxy. If the proxy has a different interface, the client must be written specifically for the proxy, and you lose the ability to swap proxy โ†” real object. Fix: Proxy and RealSubject must implement the same Subject interface. No extra methods on the proxy that the client calls.
✗ Wrong — proxy has different interface
// โœ— Proxy exposes its own API โ€” client must know it's a proxy class ImageProxy { // โœ— doesn't implement Image! void displayIfAllowed(User user) { ... } // โœ— different method name void preloadCache() { ... } // โœ— extra method } // Client can't swap ImageProxy for RealImage โ€” they have different APIs
✔ Correct — same interface, transparent
// โœ“ Proxy implements the same interface โ€” client is unaware class ImageProxy implements Image { // โœ“ same interface public void display() { // โœ“ same method // auth check, then delegate to real.display() } }
Mistake #2 — Putting business logic in the proxy: A proxy should handle cross-cutting concerns (auth, caching, logging, lazy init) — not business rules. If your proxy is calculating discounts, validating order totals, or transforming data, that logic belongs in the real object or a service layer. A proxy with business logic becomes a second service masquerading as a wrapper.
Mistake #3 — Thread-unsafe lazy initialisation:
  • The classic virtual proxy pattern if (real == null) real = new RealSubject() has a race condition in multi-threaded code.
  • Two threads can both see null and create two real objects.
  • Fix: Use synchronized, double-checked locking, or (best) make the field volatile and use the holder idiom. In Spring, this isn’t an issue because Spring manages the proxy lifecycle.
✗ Wrong — thread-unsafe lazy init
// โœ— Two threads can both create a RealImage public void display() { if (real == null) // Thread A checks: null real = new RealImage(filename); // Thread B also checks: null โ†’ two objects! real.display(); }
✔ Correct — thread-safe lazy init
private volatile RealImage real; // โœ“ volatile public void display() { if (real == null) { synchronized (this) { // โœ“ double-checked locking if (real == null) real = new RealImage(filename); } } real.display(); }
Mistake #4 — Confusing Proxy with Adapter:
  • 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.
08
Section Eight ยท Decision Guide

When To Use Proxy

Use Proxy When
  • 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
Avoid Proxy When
  • 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
Proxy vs. Related Patterns
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
Decision Flowchart
Need to wrap an object with extra behaviour? Yes No No pattern needed Same interface as the real object? No Adapter / Facade Yes Control access or can block the call? No Decorator Yes Proxy Which type? Lazy creation โ†’ Virtual Proxy Access control โ†’ Protection Proxy Cache results โ†’ Caching Proxy Remote object โ†’ Remote Proxy
09
Section Nine ยท Practice

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.
Interview Tip:
  • 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.Proxy for dynamic proxies and distinguish Proxy (same interface, access control) from Decorator (same interface, added capability) and Adapter (different interface, translation).