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
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.

Exploiting a heap buffer overflow in linux
Apr 12 Exploit development

In the [previous article](/heap-internals-how-glibc-malloc-works/), we dissected glibc's malloc — chunks, bins, tcache, coalescing, and the metadata that holds it all together. Now we break...

Common Web Application Attacks
Feb 05 Application Security

Web applications are one of the most targeted surfaces by attackers. This is primarily because they are accessible over the internet, making them exposed and potentially vulnerable. Since these...

Bypassing DEP with Return-to-libc
Apr 05 Exploit development

DEP makes the stack non-executable — our shellcode can't run. The simplest bypass? Don't inject code at all. Instead, call functions that already exist in libc. In this post, we exploit a stack overflow to call system('/bin/sh') without writing a single byte of shellcode.

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...

Singleton Pattern explained simply
Jan 27 Software Architecture

Ever needed just one instance of a class in your application? Maybe a logger, a database connection, or a configuration manager? This is where the Singleton Pattern comes in — one of the simplest but...