Skip to main content

Proxy Design Pattern in Java: Complete Guide with Examples

The Proxy pattern places a surrogate in front of an object โ€” intercepting calls to add lazy loading, access control, logging, or remote delegation without touching the real object. Complete Java guide: virtual proxy, logging proxy, proxy chaining, dynamic proxy with java.lang.reflect.Proxy, Spring AOP connection, and when each proxy type is the right fit.

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 25. 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.
Proxy design pattern structure (via refactoring.guru)
Proxy and RealSubject both implement the same interface. The Proxy holds a reference to the RealSubject and intercepts calls before/after delegating. Diagram: refactoring.guru

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.

DatabaseConnection.java
package proxy;

/**
 * Subject interface โ€” defines what both the real object and proxy expose.
 * Clients depend on this, not on the concrete class.
 */
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.

RealDatabaseConnection.java
package proxy;

/**
 * Real Subject โ€” the actual, expensive database connection.
 * Opening it takes time. We want to delay this until truly needed.
 */
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 operation)...");
        // Simulate connection setup time
        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}";  // simulated result
    }

    @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 logs that it was called but doesn’t open anything. Only executeQuery() triggers the actual connection via initIfNeeded().

LazyConnectionProxy.java
package proxy;

/**
 * Virtual Proxy โ€” delays creating the RealDatabaseConnection until
 * the first actual query is made. If no query is ever made (e.g.,
 * the service is initialized but never used in this request),
 * the expensive connection is never opened.
 *
 * This is exactly how Hibernate proxies work: entities are not
 * loaded from the database until you access a field on them.
 */
public class LazyConnectionProxy implements DatabaseConnection {

    private final String url;
    private RealDatabaseConnection real;  // null until first use

    public LazyConnectionProxy(String url) {
        this.url = url;
        System.out.println("[Proxy] Created for " + url + " (real connection NOT opened yet)");
    }

    // Lazy initialization โ€” create and connect only on first real need
    private void initIfNeeded() {
        if (real == null) {
            System.out.println("[Proxy] First access โ€” initializing real connection...");
            real = new RealDatabaseConnection(url);
            real.connect();
        }
    }

    @Override
    public void connect() {
        // Proxy absorbs the connect() call โ€” real connection opened lazily
        System.out.println("[Proxy] connect() called โ€” deferring to first query");
    }

    @Override
    public String executeQuery(String sql) {
        initIfNeeded();  // NOW we actually need the connection
        return real.executeQuery(sql);
    }

    @Override
    public void disconnect() {
        if (real != null) {
            real.disconnect();
            real = null;
        } else {
            System.out.println("[Proxy] disconnect() called but 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.

LoggingProxy.java
package proxy;

import java.time.Instant;

/**
 * Logging Proxy โ€” adds timing and audit logging around every query
 * without touching RealDatabaseConnection or any of its callers.
 *
 * This is the "cross-cutting concern" use case of Proxy,
 * the same mechanism behind Spring AOP's @Around advice.
 */
public class LoggingProxy implements DatabaseConnection {

    private final DatabaseConnection target;

    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.

Main.java
package proxy;

/**
 * Proxy Design Pattern โ€” Runnable Demo
 *
 * Shows two proxy types:
 *  1. Virtual Proxy (lazy connection)
 *  2. Logging Proxy (cross-cutting concern)
 *  3. Proxy chaining (both together)
 *
 * Run: javac proxy/*.java && java proxy.Main
 * Article: https://ankurm.com/proxy-design-pattern-java/
 */
public class Main {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== Proxy Design Pattern Demo ===\n");

        // --- Virtual Proxy: lazy connection ---
        System.out.println("-- Virtual Proxy (lazy loading) --");
        DatabaseConnection lazy = new LazyConnectionProxy("jdbc:postgresql://localhost/mydb");
        lazy.connect();  // absorbed by proxy, no real connection yet
        System.out.println("(no real connection yet โ€” saved startup time)");
        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();

        // --- Logging Proxy: wraps the real connection ---
        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();

        // --- Proxy chaining: lazy + logging ---
        System.out.println("-- Chained Proxies: Lazy + Logging --");
        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

=== Proxy Design Pattern Demo ===

— Virtual Proxy (lazy loading) —
[Proxy] Created for jdbc:postgresql://localhost/mydb (real connection NOT opened yet)
[Proxy] connect() called โ€” deferring to first query
(no real connection yet โ€” saved startup time)
[Proxy] First access โ€” initializing real connection…
[Real DB] Connecting to jdbc:postgresql://localhost/mydb (expensive operation)…
[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 operation)…
[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-24T10:11:44.155934400Z
[Real DB] Disconnecting from jdbc:mysql://localhost/shopdb

— Chained Proxies: Lazy + Logging —
[Proxy] Created for jdbc:oracle://localhost/warehouse (real connection NOT opened yet)
[Log] connect() at 2026-06-24T10:11:44.162263700Z
[Proxy] connect() called โ€” deferring to first query
[Log] QUERY START: SELECT SUM(quantity) FROM inventory
[Proxy] First access โ€” initializing real connection…
[Real DB] Connecting to jdbc:oracle://localhost/warehouse (expensive operation)…
[Real DB] Connected.
[Real DB] Executing: SELECT SUM(quantity) FROM inventory
[Log] QUERY END: 101ms | result: ResultSet{rows=42}
[Log] disconnect() at 2026-06-24T10:11:44.263526400Z
[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

Further Reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.