Proxy Pattern explained simply
Thilan Dissanayaka Software Architecture Jan 31, 2020

Proxy Pattern explained simply

Sometimes you don't want or can't allow direct access to an object. Maybe it's expensive to create, needs special permissions, or you want to control access in some way. This is where the Proxy Pattern shines.

The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it.

What is the Proxy Pattern?

At its core, the Proxy Pattern:

  • Controls access to another object (the "real subject").
  • Can add additional behavior (e.g., lazy initialization, security checks, logging).
  • Makes working with heavy or sensitive objects more efficient and safe.

Think of a security guard at a door. You can't directly enter — you must go through the guard (the proxy).

Here is a UML class diagram showing how the pieces fit together. The Client talks to the WeatherService interface and never knows whether it is dealing with the real service or the proxy.

<mxfile>
  <diagram name="Proxy Pattern">
    <mxGraphModel dx="1024" dy="768" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="500">
      <root>
        <mxCell id="0"/>
        <mxCell id="1" parent="0"/>
        <!-- WeatherService interface -->
        <mxCell id="2" value="&lt;b&gt;&amp;lt;&amp;lt;interface&amp;gt;&amp;gt;&lt;/b&gt;&#xa;&lt;b&gt;WeatherService&lt;/b&gt;&#xa;——————————————&#xa;+ getWeather(city: String): String" style="shape=rectangle;whiteSpace=wrap;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;fontSize=13;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
          <mxGeometry x="270" y="30" width="300" height="90" as="geometry"/>
        </mxCell>
        <!-- RealWeatherService -->
        <mxCell id="3" value="&lt;b&gt;RealWeatherService&lt;/b&gt;&#xa;——————————————&#xa;+ getWeather(city: String): String" style="shape=rectangle;whiteSpace=wrap;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;fontSize=13;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
          <mxGeometry x="80" y="220" width="300" height="80" as="geometry"/>
        </mxCell>
        <!-- WeatherServiceProxy -->
        <mxCell id="4" value="&lt;b&gt;WeatherServiceProxy&lt;/b&gt;&#xa;——————————————&#xa;- realService: WeatherService&#xa;- cache: Map&amp;lt;String, CacheEntry&amp;gt;&#xa;——————————————&#xa;+ getWeather(city: String): String" style="shape=rectangle;whiteSpace=wrap;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;fontSize=13;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
          <mxGeometry x="460" y="200" width="310" height="120" as="geometry"/>
        </mxCell>
        <!-- Client -->
        <mxCell id="5" value="&lt;b&gt;Client&lt;/b&gt;" style="shape=rectangle;whiteSpace=wrap;align=center;verticalAlign=middle;fontSize=13;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
          <mxGeometry x="360" y="400" width="120" height="50" as="geometry"/>
        </mxCell>
        <!-- RealWeatherService implements WeatherService -->
        <mxCell id="6" style="endArrow=block;endFill=0;endSize=14;strokeWidth=1.5;dashed=1;" edge="1" source="3" target="2" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <!-- WeatherServiceProxy implements WeatherService -->
        <mxCell id="7" style="endArrow=block;endFill=0;endSize=14;strokeWidth=1.5;dashed=1;" edge="1" source="4" target="2" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <!-- WeatherServiceProxy holds reference to RealWeatherService -->
        <mxCell id="8" value="delegates to" style="endArrow=open;endSize=12;strokeWidth=1.5;dashed=1;fontSize=11;" edge="1" source="4" target="3" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
        <!-- Client uses WeatherService (via Proxy) -->
        <mxCell id="9" value="uses" style="endArrow=open;endSize=12;strokeWidth=1.5;fontSize=11;" edge="1" source="5" target="4" parent="1">
          <mxGeometry relative="1" as="geometry"/>
        </mxCell>
      </root>
    </mxGraphModel>
  </diagram>
</mxfile>

Real-Life Analogy

Imagine you want to visit a celebrity. You can't just walk into their home. You have to talk to their agent (proxy), who controls access, manages schedules, and sometimes answers simple questions on their behalf.

Proxy Pattern in Code (Java)

Let's build something you will actually run into in the real world — an API rate-limiting and caching proxy for a weather service.

First, the interface that both the real service and the proxy will implement:

public interface WeatherService {
    String getWeather(String city);
}

The real object that hits an external API. In production this would be an HTTP call, but the idea is the same — it is slow and you get charged per request:

public class RealWeatherService implements WeatherService {

    @Override
    public String getWeather(String city) {
        // Imagine this calls an external REST API
        System.out.println("[API] Calling external weather API for: " + city);
        return "Sunny, 25°C in " + city;
    }
}

Now the proxy. It caches results and only allows one real API call per city per minute. Every other call within that window just returns the cached response:

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

public class WeatherServiceProxy implements WeatherService {

    private final WeatherService realService;
    private final Map<String, CacheEntry> cache = new HashMap<>();
    private final long ttlMillis;

    public WeatherServiceProxy(WeatherService realService, long ttlMillis) {
        this.realService = realService;
        this.ttlMillis = ttlMillis;
    }

    @Override
    public String getWeather(String city) {
        CacheEntry entry = cache.get(city);
        if (entry != null && !entry.isExpired(ttlMillis)) {
            System.out.println("[CACHE] Returning cached result for: " + city);
            return entry.data;
        }

        // Cache miss or expired — call the real service
        String result = realService.getWeather(city);
        cache.put(city, new CacheEntry(result));
        return result;
    }

    private static class CacheEntry {
        final String data;
        final long timestamp;

        CacheEntry(String data) {
            this.data = data;
            this.timestamp = System.currentTimeMillis();
        }

        boolean isExpired(long ttlMillis) {
            return System.currentTimeMillis() - timestamp > ttlMillis;
        }
    }
}

And use it like this:

public class MainProgram {
    public static void main(String[] args) throws InterruptedException {
        WeatherService service = new WeatherServiceProxy(
            new RealWeatherService(),
            60_000  // 1 minute TTL
        );

        // First call — hits the real API
        System.out.println(service.getWeather("London"));

        // Second call for the same city — served from cache
        System.out.println(service.getWeather("London"));

        // Different city — hits the real API again
        System.out.println(service.getWeather("Tokyo"));
    }
}

Output:

[API] Calling external weather API for: London
Sunny, 25°C in London
[CACHE] Returning cached result for: London
Sunny, 25°C in London
[API] Calling external weather API for: Tokyo
Sunny, 25°C in Tokyo

Notice how the client code has no idea it is talking to a proxy. It just calls getWeather() on a WeatherService. The caching and rate-limiting happen transparently behind the interface. That is the whole point.

Key Components

  • Subject Interface (WeatherService): Common interface for both the real service and the proxy.
  • RealSubject (RealWeatherService): The actual object that does the heavy lifting — in this case, calling an external API.
  • Proxy (WeatherServiceProxy): Sits in front of the real service, adding caching and rate-limiting without the client ever knowing.

When to Use the Proxy Pattern?

  • When accessing a real object is resource-intensive (e.g., external API calls, database connections).
  • When you need additional access control (e.g., authentication).
  • When you want to add lazy initialization to heavy objects.
  • When you need remote proxies (access objects over the network).

Types of Proxies

  • Virtual Proxy: Controls access to a resource that is expensive to create.
  • Protection Proxy: Controls access based on permissions.
  • Remote Proxy: Represents an object in a different address space (e.g., server).
  • Smart / Caching Proxy: Adds extra functionality like caching, rate-limiting, logging, or reference counting. Our WeatherServiceProxy above is a textbook example — it wraps the real service with a TTL-based cache so the expensive API call only happens when it actually needs to.

Advantages

  • Adds control over the real object without changing its code.
  • Supports lazy initialization and performance optimizations.
  • Adds extra responsibilities like security, caching, or logging.

Disadvantages

  • Adds complexity to the system.
  • Can create too many layers of indirection if overused.
  • Might introduce performance overhead in simple cases.

Real-World Use Cases

  • API rate-limiting and caching: Exactly what we built above — wrap an external service call with a proxy that caches responses and enforces request limits.
  • Database connections: Open only when necessary.
  • Security layers: Check permissions before allowing access.
  • Network communication: Stub objects that behave like the real remote object.
  • File systems: Load files lazily.

Final Thoughts

The Proxy Pattern is all about control — controlling access, controlling initialization, and even controlling additional behavior without touching the real object.

Whenever you need an object to stand in for another, think about using a Proxy.

Happy proxying!

ALSO READ
Understanding the Heap Internals
Apr 12 Exploit development

So far in this series, we've exploited the **stack** buffer overflows, ROP chains, format strings. The stack is predictable: local variables go in, function returns pop them out, everything follows a...

Remote Code Execution (RCE)
Jan 02 Application Security

Remote Code Execution (RCE) is the holy grail of application security vulnerabilities. It allows an attacker to execute arbitrary code on a remote server — and the consequences are as bad as it sounds. In this post, we'll go deep into RCE across multiple languages, including PHP, Java, Python, and Node.js.

Exploiting a Stack Buffer Overflow on Windows
Apr 12 Exploit development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

SQL Injection Login Bypass
Feb 10 Application Security

SQL Injection (SQLi) is one of the oldest and most fundamental web application vulnerabilities. While modern frameworks have made it harder to introduce, understanding SQL injection is essential for anyone learning web security. In this post, we'll break it down from the ground up using a classic login bypass.

How I built a web based CPU Simulator
May 07 Pet Projects

As someone passionate about computer engineering, reverse engineering, and system internals, I've always been fascinated by what happens "under the hood" of a computer. This curiosity led me to...

Boolean Based Blind SQL Injection
Feb 12 Application Security

In regular SQL injection, you can see the database output right there on the page. Blind SQL injection is different — the application gives you nothing. No errors, no data, no feedback. But with boolean-based blind SQLi, you can still extract the entire database — one true/false question at a time.