Loading episodes…
0:00 0:00

Proxy Pattern Visually Explained: Your Guide to Smart Object Control

00:00
BACK TO HOME

Proxy Pattern Visually Explained: Your Guide to Smart Object Control

10xTeam December 28, 2025 9 min read

The Proxy Design Pattern is a structural pattern that lets you provide a substitute or placeholder for another object. But why would you want to do that? A proxy controls access to the original object, allowing you to perform additional logic before or after the request gets to the original object.

Think of it like a security guard in front of a building. You can’t just walk into the main office (the Real Object). You first have to talk to the guard (the Proxy). The guard might check your ID, log your visit time, or deny you access altogether. The guard controls access and adds extra functionality without changing the office itself.

Let’s visualize this relationship:

graph TD
    subgraph "Client's Perspective"
        Client
    end

    subgraph "Service Layer"
        Proxy -- intercepts --> RealObject
    end

    Client -- interacts with --> Proxy

    style RealObject fill:#f9f,stroke:#333,stroke-width:2px
    style Proxy fill:#bbf,stroke:#333,stroke-width:2px

The client thinks it’s talking to the real object, but it’s actually interacting with the proxy. This seamless substitution is the core strength of the pattern.

Why Do We Need the Proxy Pattern?

The Proxy pattern is incredibly versatile. It’s not just for one specific problem but is a solution to a whole category of them. Here are its most common applications:

mindmap
  root((Proxy Use Cases))
    Access Control
      ::icon(fa fa-shield-alt)
      Protects the real object from unauthorized clients.
      Example: A user proxy that checks permissions before allowing access to sensitive admin functions.
    Lazy Initialization (Virtual Proxy)
      ::icon(fa fa-hourglass-half)
      Delays the creation of a resource-intensive object until it's truly needed.
      Example: An image proxy that only loads a high-resolution image when the user clicks on it.
    Caching (Caching Proxy)
      ::icon(fa fa-database)
      Stores the results of expensive operations and serves them from a cache to improve performance.
      Example: Our video downloader example below!
    Logging & Auditing
      ::icon(fa fa-file-alt)
      Logs requests and responses before and after they reach the real object.
      Example: A database proxy that logs every SQL query executed.
    Failure Protection (Protection Proxy)
      ::icon(fa fa-broadcast-tower)
      Can provide fallback behavior, like retrying a failed network request or implementing a circuit breaker.
      Example: An API proxy that retries a request if the remote server is temporarily unavailable.

Case Study: Building a High-Performance Caching Video Downloader

Let’s make this concrete. Imagine we have a service that downloads videos from the internet. A direct implementation might be slow if users repeatedly request the same video.

The Initial Problem: No Caching

First, let’s define a common interface for our downloaders.

[!TIP] Using an interface is crucial. It ensures that the client, the real object, and the proxy all conform to the same contract, making them interchangeable. This is known as “programming to an interface.”

Here’s our project structure:

src/main/java/com/example/
├── service/
│   ├── VideoDownloader.java      // Interface
│   ├── RealVideoDownloader.java  // Real Subject
│   └── CachingVideoProxy.java    // Proxy
└── Main.java                     // Client

VideoDownloader.java (The Interface)

package com.example.service;

// The common interface for both the Real Subject and the Proxy
public interface VideoDownloader {
    String downloadVideo(String url);
}

RealVideoDownloader.java (The Real Subject) This class does the “heavy lifting” of downloading a video from a URL. We’ll simulate the delay.

package com.example.service;

// The "heavy" object that does the real work
public class RealVideoDownloader implements VideoDownloader {

    @Override
    public String downloadVideo(String url) {
        System.out.println("Connecting to internet and downloading video from: " + url);
        // Simulate a network delay
        try {
            Thread.sleep(4000); // 4-second delay
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("Download interrupted.");
        }
        System.out.println("-> Download complete for: " + url);
        return "VideoData[" + url + "]";
    }
}

Main.java (The Client) The client directly uses RealVideoDownloader. Notice the performance hit when we request the same video twice.

package com.example;

import com.example.service.RealVideoDownloader;
import com.example.service.VideoDownloader;

public class Main {
    public static void main(String[] args) {
        VideoDownloader downloader = new RealVideoDownloader();

        System.out.println("--- First Request ---");
        downloader.downloadVideo("https://example.com/video1.mp4"); // Takes 4s

        System.out.println("\n--- Second Request (Same Video) ---");
        downloader.downloadVideo("https://example.com/video1.mp4"); // Takes another 4s!

        System.out.println("\n--- Third Request ---");
        downloader.downloadVideo("https://example.com/video2.mp4"); // Takes 4s
    }
}

Each download takes 4 seconds, even for the same URL. We can do much better!

The Solution: A Caching Proxy

Now, let’s introduce a CachingVideoProxy. This proxy will sit between the client and the RealVideoDownloader. It will maintain a cache of already downloaded videos.

Here’s the class structure we’re aiming for:

classDiagram
    direction LR
    class Client
    class VideoDownloader {
        <<interface>>
        +downloadVideo(url) string
    }
    class RealVideoDownloader {
        +downloadVideo(url) string
    }
    class CachingVideoProxy {
        -realDownloader: RealVideoDownloader
        -videoCache: Map<String, String>
        +downloadVideo(url) string
    }

    Client ..> VideoDownloader : uses
    VideoDownloader <|.. RealVideoDownloader : implements
    VideoDownloader <|.. CachingVideoProxy : implements
    CachingVideoProxy o-- RealVideoDownloader : has a

CachingVideoProxy.java (The Proxy) This class implements the same VideoDownloader interface but adds the caching logic.

package com.example.service;

import java.util.HashMap;
import java.util.Map;

// The Proxy object that adds caching
public class CachingVideoProxy implements VideoDownloader {
    private final RealVideoDownloader realDownloader;
    private final Map<String, String> videoCache = new HashMap<>();

    public CachingVideoProxy() {
        // The proxy creates and manages the real object
        this.realDownloader = new RealVideoDownloader();
    }

    @Override
    public String downloadVideo(String url) {
        // 1. Check the cache first
        if (videoCache.containsKey(url)) {
            System.out.println("Found in cache! Serving video from: " + url);
            return videoCache.get(url);
        }

        // 2. If not in cache, delegate to the real downloader
        System.out.println("Not in cache. Delegating to real downloader...");
        String videoData = realDownloader.downloadVideo(url);

        // 3. Store the result in the cache for future requests
        videoCache.put(url, videoData);
        System.out.println("-> Stored in cache: " + url);

        return videoData;
    }
}

The flow of a request now looks like this:

sequenceDiagram
    participant Client
    participant Proxy as CachingVideoProxy
    participant Real as RealVideoDownloader

    Client->>Proxy: downloadVideo("video1.mp4")
    Proxy->>Proxy: Is "video1.mp4" in cache? (No)
    Proxy->>Real: downloadVideo("video1.mp4")
    Note over Real: (Simulates 4s delay)
    Real-->>Proxy: returns VideoData
    Proxy->>Proxy: Store "video1.mp4" in cache
    Proxy-->>Client: returns VideoData

    Client->>Proxy: downloadVideo("video1.mp4")
    Proxy->>Proxy: Is "video1.mp4" in cache? (Yes)
    Note over Proxy: (Returns immediately)
    Proxy-->>Client: returns VideoData from cache

Refactoring the Client to Use the Proxy

The final step is to update our client. The change is minimal—we just instantiate CachingVideoProxy instead of RealVideoDownloader.

- import com.example.service.RealVideoDownloader;
+ import com.example.service.CachingVideoProxy;
  import com.example.service.VideoDownloader;
  
  public class Main {
      public static void main(String[] args) {
-         VideoDownloader downloader = new RealVideoDownloader();
+         VideoDownloader downloader = new CachingVideoProxy();
  
          System.out.println("--- First Request ---");
          downloader.downloadVideo("https://example.com/video1.mp4"); // Takes 4s
  
          System.out.println("\n--- Second Request (Same Video) ---");
          downloader.downloadVideo("https://example.com/video1.mp4"); // Instant!
  
          System.out.println("\n--- Third Request ---");
          downloader.downloadVideo("https://example.com/video2.mp4"); // Takes 4s
      }
  }

Now, the second request for video1.mp4 is served instantly from the cache, dramatically improving performance. The client code barely changed, but the system’s behavior is significantly better.

Deep Dive: Other Types of Proxies
Our example was a **Caching Proxy**, but there are other common types: * **Virtual Proxy**: Manages an object that is expensive to create. The proxy delays the creation of the `Real Object` until it is absolutely necessary (e.g., a method on it is called). This is a form of lazy initialization. * **Protection (or Protective) Proxy**: Controls access to the methods of the `Real Object`. It checks if the caller has the required permissions before forwarding the request. This is useful for implementing security layers. * **Remote Proxy**: Provides a local representation for an object that exists in a different address space (e.g., on a remote server). The remote proxy handles the details of network communication, making the remote object feel like a local one.

Best Practices and Potential Pitfalls

[!TIP] Keep Proxies Focused: A proxy should ideally handle one responsibility (e.g., caching or access control). If you need both, you can chain proxies together (a client talks to a caching proxy, which talks to a security proxy, which talks to the real object).

[!WARNING] Cache Invalidation is Hard: In our caching example, what if the video at video1.mp4 is updated? Our cache will serve the old, stale version. Real-world caching proxies need a strategy for cache invalidation (e.g., time-to-live (TTL) timers) to keep data fresh.

Conclusion

The Proxy pattern is a powerful tool for managing object interactions. By inserting a surrogate object, you can add behavior like caching, security, and lazy loading without the client or the real object ever knowing. It allows you to build systems that are more performant, secure, and robust, all while keeping your code clean and decoupled.


Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?