Structural Design Pattern: Proxy

The Proxy pattern provides a substitute for another object, controlling access and allowing additional functionality before or after requests reach the original object; useful for lazy loading, security, and resource management.Retry

This article is the first in a series exploring design patterns using the Java programming language. The goal of this series is to help readers develop a solid understanding of design patterns while also sharing real-world examples from actual codebases that make use of these patterns.

In this article, we’ll be discussing the Proxy pattern.

Symbols for better navigation:

🤔 Hypothetical Scenario/Imagination

⚠️ Warning

👉 Point to note

📝 Common Understanding

Proxy Pattern

The Proxy pattern is a structural design pattern that provides a substitute or placeholder for another object.

A proxy controls access to the original object, allowing you to perform something either before or after the request gets through to the original object.

🤔 Imagine you’re staying at a hotel and you need restaurant reservations, theater tickets, or a taxi. You don’t call these services directly; instead, you go through the hotel concierge. The concierge acts as a proxy, handling your requests, potentially adding value (like knowing the best restaurants), and managing access to external services.

This is exactly what the Proxy pattern does in software design.

The Proxy pattern is useful when you want to:

  • Control access to an object (security proxy).
  • Delay expensive operations until they’re actually needed (lazy initialization).
  • Add functionality without changing the original object (logging, caching).
  • Work with remote objects as if they were local (remote proxy).
  • Manage resources efficiently (virtual proxy).

Recipe to cook the Proxy Pattern for objects

The Proxy pattern involves creating a proxy class that implements the same interface as the original object.

This proxy either forwards requests to the real object or handles them directly, depending on the use case.

Here are the steps to implement a proxy:

  1. Define a common interface that both the real object and the proxy will implement.
  2. Create the real subject class that implements this interface and contains the actual business logic.
  3. Create the proxy class that also implements the same interface.
  4. Inside the proxy, maintain a reference to the real subject.
  5. Implement methods in the proxy that either:
    • Forward calls to the real subject
    • Add additional behavior before/after forwarding
    • Or handle the request entirely without forwarding

Let’s create a practical example using image loading.

First, we define the interface:

Java
public interface Image {
    void display();
    void load();
}

Next, we create the real subject that does the actual work:

Java
public class RealImage implements Image {
    private String filename;
    private byte[] imageData;
    
    public RealImage(String filename) {
        this.filename = filename;
    }
    
    @Override
    public void load() {
        System.out.println("Loading image from disk: " + filename);
        // Simulate expensive operation
        try {
            Thread.sleep(2000); // Takes 2 seconds
            this.imageData = new byte[1024 * 1024]; // 1MB of data
            System.out.println("Image loaded: " + filename);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void display() {
        if (imageData == null) {
            System.out.println("Cannot display - image not loaded!");
            return;
        }
        System.out.println("Displaying image: " + filename);
    }
}

Now, we create the proxy that controls access.

Java
// Proxy - Controls access and adds lazy loading
public class ImageProxy implements Image {
    private RealImage realImage;
    private String filename;
    private boolean isLoaded = false;
    
    public ImageProxy(String filename) {
        this.filename = filename;
    }
    
    @Override
    public void load() {
        if (!isLoaded) {
            if (realImage == null) {
                realImage = new RealImage(filename);
            }
            realImage.load();
            isLoaded = true;
        } else {
            System.out.println("Image already loaded: " + filename);
        }
    }
    
    @Override
    public void display() {
        if (!isLoaded) {
            System.out.println("Loading image on-demand...");
            load();
        }
        realImage.display();
    }
}

Now let’s see how we use this proxy.

Java
public class ProxyDemo {
    public static void main(String[] args) {
        // Create proxy instead of real object
        Image image1 = new ImageProxy("photo1.jpg");
        Image image2 = new ImageProxy("photo2.jpg");
        
        // Images are NOT loaded yet - fast instantiation!
        System.out.println("Image objects created (not loaded)");
        
        // Only load when needed
        image1.display(); // This will trigger loading
        image1.display(); // This will use already-loaded image
        
        // image2 is never loaded because we never display it
        // Saved time and memory!
    }
}

🤔 Notice how the proxy delays the expensive loading operation until it’s actually needed? This is called lazy initialization, and it’s one of the most common uses of the Proxy pattern.

Case-Study

Let’s look at a real-world scenario where the Proxy pattern shines: database connection management.

🤔 Imagine you’re building a web application that handles thousands of concurrent user requests. Each request might need to query the database.

⚠️ Creating a new database connection for every request is extremely expensive as it involves network overhead, authentication, and resource allocation.

Instead, we use a connection pool with a proxy pattern to manage connections efficiently.

Here’s how it works.

Java
public interface DatabaseConnection {
    void executeQuery(String query);
    void close();
}
Java
// Real database connection - expensive to create
public class RealDatabaseConnection implements DatabaseConnection {
    private String connectionId;
    
    public RealDatabaseConnection(String connectionId) {
        this.connectionId = connectionId;
        System.out.println("Creating expensive DB connection: " + connectionId);
        // Simulate connection creation time
        try {
            Thread.sleep(1000); // Takes 1 second to establish
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void executeQuery(String query) {
        System.out.println("[" + connectionId + "] Executing: " + query);
    }
    
    @Override
    public void close() {
        System.out.println("[" + connectionId + "] Connection closed");
    }
}

This pattern provides several benefits:

  • Resource optimization – Reuses expensive connections.
  • Performance – Eliminates repeated connection creation overhead.
  • Control – Limits the total number of connections.
  • Transparency – Client code remains simple and unaware of pooling.

📝 Most database frameworks (like HikariCP, Apache DBCP) use variations of this proxy pattern for connection pooling.

Real-World Use Case of Proxy

One excellent real-world example of the Proxy pattern in the Java standard library is the java.net.URL class.

👉 The java.net.URL class acts as a proxy for resources located on the network. When you create a URL object, it doesn’t immediately fetch the content from the network; that would be wasteful if you never actually read the data.

Instead, URL acts as a lazy proxy that only connects and retrieves data when you explicitly request it.

Here’s how it demonstrates the Proxy pattern.

Java
import java.net.URL;
import java.io.InputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class URLProxyExample {
    public static void main(String[] args) {
        try {
            // Creating URL object - no network call yet!
            URL url = new URL("https://api.github.com/users/amritpandey23");
            System.out.println("URL object created (no network call)");
            
            // Only when we call openStream(), the actual connection happens
            System.out.println("Opening connection...");
            InputStream stream = url.openStream();
            
            // Now data is fetched
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(stream)
            );
            
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            
            reader.close();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The URL class exhibits classic proxy behavior:

  1. Delayed execution – The URL object is lightweight; the actual network connection and data fetching happen only when openStream() or openConnection() is called.
  2. Same interface – Whether you’re accessing a local file (file://) or a remote HTTP resource (https://), the interface remains consistent.
  3. Controlled access – The URL class manages the complexity of:
    • Protocol handling (HTTP, HTTPS, FTP, etc.)
    • Connection management
    • Data streaming
    • Error handling
  4. Additional functionality – The proxy adds capabilities like:
    • URL validation
    • Protocol negotiation
    • Content type detection
    • Caching (through URLConnection)

Up next in the Series

Decorator Design Pattern

Subscribe to my newsletter today!

Share it on

Leave a Reply

Your email address will not be published. Required fields are marked *