Your service holds a database connection that takes 200ms to open. On most requests you don’t need it — the data is already cached or the request is served from memory. With the Proxy pattern, you can give every caller a DatabaseConnection object at startup but only open the real connection the first time a query is actually executed. The caller’s code doesn’t change. The real connection object doesn’t change. The proxy sits in between and makes the decision.
Proxy is one of the most widely used patterns in enterprise Java. Every time you use Spring’s @Transactional, @Cacheable, or @Async, you’re working through a proxy. Hibernate’s lazy-loaded entities are proxies. Java’s java.lang.reflect.Proxy creates proxies dynamically at runtime. Understanding the pattern means understanding a large chunk of how modern frameworks work.
All code compiles and runs with Java 17. No external dependencies required.
Three Common Proxy Types
The structure is the same in all cases — a proxy implements the same interface as the real object and holds a reference to it — but the reason for intercepting varies:
- Virtual Proxy — delays expensive object creation until it’s actually needed (lazy initialisation). Hibernate entity proxies, Spring’s lazy beans.
- Protection Proxy — controls access based on caller permissions. The proxy checks whether the caller is authorised before delegating to the real object.
- Logging / Auditing Proxy — adds cross-cutting concerns (timing, logging, metrics) around every call without modifying the real object. Spring AOP is built on this.
- Remote Proxy — makes a remote object look local. Java RMI stubs are remote proxies; the stub implements the remote interface and handles serialization and network calls.

The Subject Interface
The interface is the contract that both the real object and every proxy implement. Clients depend only on this — they don’t know whether they have the real thing or a proxy.
package proxy;
/**
* Subject interface — the contract that both the real object and all proxies satisfy.
* Clients hold this type and never reference RealDatabaseConnection directly.
*/
public interface DatabaseConnection {
void connect();
String executeQuery(String sql);
void disconnect();
}
The Real Subject
The actual database connection. Opening it is expensive — the simulated 100ms sleep represents a real TCP handshake, SSL negotiation, and authentication round-trip. This is the object we want to create lazily and wrap with logging.
package proxy;
/**
* Real Subject — the expensive object we want to proxy.
* Creating and connecting takes time; we only want to do it when necessary.
*/
public class RealDatabaseConnection implements DatabaseConnection {
private final String url;
public RealDatabaseConnection(String url) { this.url = url; }
@Override
public void connect() {
System.out.println("[Real DB] Connecting to " + url + " (expensive)...");
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("[Real DB] Connected.");
}
@Override
public String executeQuery(String sql) {
System.out.println("[Real DB] Executing: " + sql);
return "ResultSet{rows=42}";
}
@Override
public void disconnect() {
System.out.println("[Real DB] Disconnecting from " + url);
}
}
Virtual Proxy: Lazy Initialisation
This proxy hands a DatabaseConnection to callers immediately, but defers creating the real connection until the first query is executed. The connect() call is absorbed — the proxy says “noted” but doesn’t open anything. Only executeQuery() triggers the actual connection via initIfNeeded().
package proxy;
/**
* Virtual Proxy — defers creating RealDatabaseConnection until first use.
*
* If the connection is never needed (request served from cache, early return,
* feature flag off), the expensive setup never runs. Same idea as Hibernate's
* lazy entity loading: the proxy object exists immediately; the DB hit happens
* only when you access a field on the entity.
*/
public class LazyConnectionProxy implements DatabaseConnection {
private final String url;
private RealDatabaseConnection real; // null until first query
public LazyConnectionProxy(String url) {
this.url = url;
System.out.println("[Proxy] Created for " + url + " (real connection NOT opened)");
}
private void initIfNeeded() {
if (real == null) {
System.out.println("[Proxy] First access — initialising real connection...");
real = new RealDatabaseConnection(url);
real.connect();
}
}
@Override
public void connect() {
// Proxy absorbs this — we'll connect when the first query arrives
System.out.println("[Proxy] connect() noted — deferring to first query");
}
@Override
public String executeQuery(String sql) {
initIfNeeded(); // THIS is when the real connection opens
return real.executeQuery(sql);
}
@Override
public void disconnect() {
if (real != null) {
real.disconnect();
real = null;
} else {
System.out.println("[Proxy] disconnect() — connection was never opened");
}
}
}
Logging Proxy: Cross-Cutting Concerns
This proxy wraps any DatabaseConnection and adds timing and audit logging around every call. Notice it takes a DatabaseConnection reference — not a RealDatabaseConnection — so it can wrap other proxies too. That composability is the key to proxy chaining.
package proxy;
import java.time.Instant;
/**
* Logging Proxy — intercepts every call and adds timing + audit logging.
* The target knows nothing about logging; callers know nothing about timing.
* This is exactly what Spring AOP's @Around advice does under the hood —
* the framework generates a proxy class implementing your service's interface,
* and injects the around-advice before and after the delegated call.
*/
public class LoggingProxy implements DatabaseConnection {
private final DatabaseConnection target; // any DatabaseConnection, not just Real
public LoggingProxy(DatabaseConnection target) { this.target = target; }
@Override
public void connect() {
System.out.println("[Log] connect() at " + Instant.now());
target.connect();
}
@Override
public String executeQuery(String sql) {
long start = System.currentTimeMillis();
System.out.println("[Log] QUERY START: " + sql);
String result = target.executeQuery(sql);
long elapsed = System.currentTimeMillis() - start;
System.out.println("[Log] QUERY END: " + elapsed + "ms | result: " + result);
return result;
}
@Override
public void disconnect() {
System.out.println("[Log] disconnect() at " + Instant.now());
target.disconnect();
}
}
Wiring It Together: Proxy Chaining
Because every proxy implements DatabaseConnection, you can wrap proxies inside other proxies. The outermost proxy runs first; each layer delegates inward. This is the same model as the Decorator pattern — and the distinction is intent: Decorator adds behaviour to enrich an object; Proxy controls access to or defers creation of an object.
package proxy;
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Proxy Design Pattern Demo ===\n");
// 1. Virtual Proxy alone
System.out.println("-- Virtual Proxy (lazy loading) --");
DatabaseConnection lazy = new LazyConnectionProxy("jdbc:postgresql://localhost/mydb");
lazy.connect(); // absorbed — no real connection yet
System.out.println("(no real connection opened yet — saved 100ms startup)");
System.out.println("Result: " + lazy.executeQuery("SELECT * FROM users WHERE id=1"));
System.out.println("Result: " + lazy.executeQuery("SELECT COUNT(*) FROM orders"));
lazy.disconnect();
System.out.println();
// 2. Logging Proxy wrapping the real connection directly
System.out.println("-- Logging Proxy --");
DatabaseConnection real = new RealDatabaseConnection("jdbc:mysql://localhost/shopdb");
real.connect();
DatabaseConnection logged = new LoggingProxy(real);
logged.executeQuery("SELECT * FROM products LIMIT 10");
logged.disconnect();
System.out.println();
// 3. Chained: LoggingProxy wraps LazyProxy wraps Real
// Call chain: Logging → Lazy → Real
System.out.println("-- Chained Proxies: Logging + Lazy --");
DatabaseConnection chain =
new LoggingProxy(
new LazyConnectionProxy("jdbc:oracle://localhost/warehouse"));
chain.connect();
chain.executeQuery("SELECT SUM(quantity) FROM inventory");
chain.disconnect();
System.out.println("\n=== Demo complete ===");
}
}
Console Output
— Virtual Proxy (lazy loading) —
[Proxy] Created for jdbc:postgresql://localhost/mydb (real connection NOT opened)
[Proxy] connect() noted — deferring to first query
(no real connection opened yet — saved 100ms startup)
[Proxy] First access — initialising real connection…
[Real DB] Connecting to jdbc:postgresql://localhost/mydb (expensive)…
[Real DB] Connected.
[Real DB] Executing: SELECT * FROM users WHERE id=1
Result: ResultSet{rows=42}
[Real DB] Executing: SELECT COUNT(*) FROM orders
Result: ResultSet{rows=42}
[Real DB] Disconnecting from jdbc:postgresql://localhost/mydb
— Logging Proxy —
[Real DB] Connecting to jdbc:mysql://localhost/shopdb (expensive)…
[Real DB] Connected.
[Log] QUERY START: SELECT * FROM products LIMIT 10
[Real DB] Executing: SELECT * FROM products LIMIT 10
[Log] QUERY END: 0ms | result: ResultSet{rows=42}
[Log] disconnect() at 2026-06-13T…
[Real DB] Disconnecting from jdbc:mysql://localhost/shopdb
— Chained Proxies: Logging + Lazy —
[Proxy] Created for jdbc:oracle://localhost/warehouse (real connection NOT opened)
[Log] connect() at 2026-06-13T…
[Proxy] connect() noted — deferring to first query
[Log] QUERY START: SELECT SUM(quantity) FROM inventory
[Proxy] First access — initialising real connection…
[Real DB] Connecting to jdbc:oracle://localhost/warehouse (expensive)…
[Real DB] Connected.
[Real DB] Executing: SELECT SUM(quantity) FROM inventory
[Log] QUERY END: 101ms | result: ResultSet{rows=42}
[Log] disconnect() at 2026-06-13T…
[Real DB] Disconnecting from jdbc:oracle://localhost/warehouse
=== Demo complete ===
Dynamic Proxy with java.lang.reflect.Proxy
For logging, timing, and access-control proxies that wrap arbitrary interfaces, Java’s built-in java.lang.reflect.Proxy generates the proxy class at runtime without you having to write boilerplate for every method. This is how Spring AOP, Mockito mocks, and many ORM frameworks generate their proxies.
import java.lang.reflect.*;
// Create a dynamic proxy that logs every method call on any DatabaseConnection
DatabaseConnection target = new RealDatabaseConnection("jdbc:h2:mem:test");
target.connect();
DatabaseConnection dynamicProxy = (DatabaseConnection) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{ DatabaseConnection.class },
(proxy, method, args) -> {
System.out.println("[DynProxy] Calling: " + method.getName());
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
System.out.println("[DynProxy] Done in " + (System.currentTimeMillis() - start) + "ms");
return result;
}
);
dynamicProxy.executeQuery("SELECT 1");
// No boilerplate per-method — the InvocationHandler intercepts all calls uniformly
The InvocationHandler‘s invoke method receives the method reference and arguments for every call made on the proxy. This is exactly what Spring does when you annotate a bean with @Transactional — it generates a proxy that wraps the transaction begin/commit/rollback around your method’s method.invoke(target, args).
💡 Spring AOP and CGLIB: Spring’s AOP uses two proxy mechanisms. For beans that implement interfaces, it uses java.lang.reflect.Proxy (JDK dynamic proxies). For classes that don’t implement interfaces, it uses CGLIB to generate a subclass at runtime. In both cases the caller receives a proxy, not the real bean. This is why @Transactional doesn’t work when you call a method on this from within the same class — you’re bypassing the proxy entirely, calling the real object directly.
Proxy vs Decorator vs Adapter
All three patterns wrap an object behind an interface. The differences are in intent and direction. Decorator adds behaviour to enrich an object — a logging decorator makes an object more capable. Proxy controls access to an object — a virtual proxy defers its creation; a protection proxy guards it. Adapter changes the interface — the caller’s expected interface is different from the wrapped object’s interface, so the adapter translates between them. In Proxy and Decorator, the interface stays the same throughout; in Adapter, translation is the whole point.
When to Use Proxy
Reach for Proxy when: you need lazy initialisation of a heavy resource that may not be used (virtual proxy). You need access control without modifying the subject (protection proxy). You want to add cross-cutting concerns — logging, caching, metrics, transaction management — without touching the subject (logging/caching proxy). You need a local representative for a remote object (remote proxy). You want to add reference counting or cleanup logic around a shared resource.
Avoid it when: the overhead of the proxy call is meaningful at the scale you’re operating — every proxy delegation is an extra method call. You’re adding proxies speculatively without a concrete concern to address. Your subject interface is large and generating a proxy for every method is more noise than signal — consider whether a narrower interface might be better.
✅ Keep the Interface Small: Proxy’s boilerplate cost is proportional to the number of methods on the interface. A DatabaseConnection interface with 3 methods is manageable. An interface with 40 methods means 40 pass-through methods per proxy — unless you use dynamic proxies. When you know a proxy is likely, keep interfaces focused and small. A QueryExecutor with one execute(String sql) method is much cleaner to proxy than a full Connection with dozens of methods.
Runnable Code on GitHub
The complete source for this article is at ankurm.com/git.app/asmhatre/design-patterns under 02-structural/proxy/. Run it with:
javac proxy/*.java -d out/proxy
java -cp out/proxy proxy.Main
See Also
- Decorator Design Pattern in Java — also wraps behind the same interface, but to enrich rather than control access; Proxy and Decorator are often confused
- Adapter Design Pattern in Java — wraps but with interface translation; Proxy preserves the same interface
- Flyweight Design Pattern in Java — virtual proxies for unloaded entities and flyweight shared objects serve similar memory-saving goals from different angles
Further Reading
- Proxy — Refactoring.Guru
- The Proxy Pattern in Java — Baeldung
- java.lang.reflect.Proxy — Java 17 API
- Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides, Chapter 4