How to Implement the Proxy Pattern for Lazy Initialization in Java

The Proxy pattern enables lazy initialization by interposing a surrogate object that instantiates the expensive real subject only when its methods are first invoked, minimizing resource usage until absolutely necessary.

The java-design-patterns repository provides a complete reference implementation demonstrating how to defer costly object creation using the Virtual Proxy subtype. This approach ensures that heavy initialization logic—such as loading large media files or establishing database connections—executes only when the client actually requests the operation, not at application startup.

Understanding the Virtual Proxy Pattern

A Virtual Proxy acts as a lightweight placeholder for an object that is expensive to create or resource-intensive to maintain. It implements the same interface as the real subject, allowing client code to remain agnostic about whether it holds a proxy or the actual object. The proxy intercepts method calls and performs lazy instantiation, forwarding all subsequent requests to the newly created real instance.

Core Source Files in java-design-patterns

According to the source code in iluwatar/java-design-patterns, the Virtual Proxy implementation resides in the virtual-proxy module. The four critical files that compose this pattern are:

Step-by-Step Implementation

1. Define the Service Interface

Both the real subject and the proxy implement a common interface. This abstraction ensures the client depends only on the contract, not on concrete classes.

public interface ExpensiveObject {
    void process();
}

2. Implement the Real Subject

The real subject encapsulates the heavy initialization work inside its constructor. In RealVideoObject.java, the heavyInitialConfiguration() method simulates this costly setup.

@Slf4j
public class RealVideoObject implements ExpensiveObject {

    public RealVideoObject() {
        heavyInitialConfiguration();
    }

    private void heavyInitialConfiguration() {
        LOGGER.info("Loading initial video configurations...");
    }

    @Override
    public void process() {
        LOGGER.info("Processing and playing video content...");
    }
}

3. Build the Lazy-Loading Proxy

The VideoObjectProxy class holds a null reference to RealVideoObject until the client invokes process(). On the first call, it constructs the real object and caches the reference for future delegations.

@Getter
public class VideoObjectProxy implements ExpensiveObject {

    private RealVideoObject realVideoObject;

    @Override
    public void process() {
        if (realVideoObject == null) {
            realVideoObject = new RealVideoObject();
        }
        realVideoObject.process();
    }
}

4. Wire the Client Application

The client instantiates the proxy rather than the real object. This configuration keeps the expensive constructor from running until the first process() call.

public class App {
    public static void main(String[] args) {
        ExpensiveObject video = new VideoObjectProxy();
        video.process(); // Triggers RealVideoObject creation
        video.process(); // Uses cached instance
    }
}

Thread-Safe Proxy Implementation

The basic implementation above is not thread-safe. For concurrent environments, apply double-checked locking to ensure only one thread initializes the real object. Mark the reference as volatile to prevent instruction reordering issues.

public class ThreadSafeVideoObjectProxy implements ExpensiveObject {

    private volatile RealVideoObject realVideoObject;

    @Override
    public void process() {
        if (realVideoObject == null) {
            synchronized (this) {
                if (realVideoObject == null) {
                    realVideoObject = new RealVideoObject();
                }
            }
        }
        realVideoObject.process();
    }
}

Key Benefits of This Approach

  • Performance Optimization: Expensive resources allocate only when required, reducing application startup time and memory footprint.
  • Encapsulation: Client code remains decoupled from object creation logic, depending solely on the ExpensiveObject interface.
  • Extensibility: The proxy can transparently add cross-cutting concerns such as logging, access control, or result caching before delegating to the real subject.

Summary

  • The Virtual Proxy pattern defers instantiation of costly objects until their first use.
  • Implementation requires a shared interface (ExpensiveObject), a real subject (RealVideoObject), and a proxy (VideoObjectProxy) that manages lazy creation.
  • Source files are located in the virtual-proxy module of iluwatar/java-design-patterns.
  • For multi-threaded scenarios, implement double-checked locking using a volatile reference to maintain thread safety without synchronizing every method call.

Frequently Asked Questions

What distinguishes a Virtual Proxy from a Decorator?

A Virtual Proxy controls access to an object primarily to delay its creation or manage resource-intensive instantiation, while a Decorator adds responsibilities or behaviors to an object dynamically without changing its interface. The proxy’s intent is lazy initialization; the decorator’s intent is functional enhancement.

When should I use lazy initialization with a Proxy?

Use this pattern when an object is expensive to create (consumes significant memory, CPU, or I/O) and its use is infrequent or uncertain during the application lifecycle. Examples include loading large images, initializing database connections, or parsing massive configuration files.

Is the standard Proxy implementation thread-safe by default?

No, the basic implementation in VideoObjectObject.java is not thread-safe. Without synchronization, multiple threads could simultaneously pass the null check and create multiple instances of the real subject. Use the double-checked locking pattern or initialize the proxy within a synchronized factory method for concurrent environments.

How does the Virtual Proxy differ from a Remote Proxy or Protection Proxy?

The Virtual Proxy handles lazy initialization of expensive local objects. A Remote Proxy manages communication with objects residing in different address spaces (network transparency), while a Protection Proxy controls access permissions based on authentication levels. All three share the same structural interface but serve distinct functional purposes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →