Sitemap

Proxy Pattern Is Not About Caching — It’s About Control

8 min readMay 22, 2025

--

In our previous article, Class Explosion Is Real — Bridge Can Stop It, we tackled an overgrown class hierarchy with the Bridge pattern. Now we turn to the Proxy pattern — another structural solution, but for a different problem: controlling access to objects.

Need to enforce security checks or delay expensive operations until necessary? That’s exactly what the Proxy pattern helps with. A proxy is essentially a stand-in for a real object. It intercepts calls to that object, allowing you to add an extra layer of control or optimization (like access control, lazy initialization, caching, etc.) behind the scenes.

Proxy Design Pattern

This article will demystify the Proxy pattern in depth. We’ll explain what Proxy is and how it works, and use a real-world analogy to illustrate the concept. We’ll also compare Proxy with similar patterns (Decorator and Adapter), explore practical use cases such as lazy loading and security enforcement, and discuss implementation challenges. Finally, we’ll look at an example in Dart that shows a security-check Proxy in action. By the end, you’ll know when to apply the Proxy pattern in your projects — and when not to. Let’s dive in!

Description

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. Instead of calling the real object directly, the client calls the proxy object, which then forwards the request to the real object (possibly adding some extra behavior before or after the forwarding).

A Proxy implements the same interface as the real object, so it can be used interchangeably with the real object from the client’s perspective. Under the hood, the proxy maintains a reference to the real object. When a method is called on the proxy, it will delegate that call to the real object, optionally performing additional actions around it.

Proxy pattern diagram — Proxy pattern — Wikipedia

Key features of the Proxy pattern include:

  • Access Control: The proxy can regulate access to the real object. For example, it might check user permissions or an access token before allowing a method call to proceed.
  • Lazy Initialization: A proxy can delay the creation or loading of an expensive object until it’s actually needed.
  • Additional Functionality: A proxy can add extra behavior on the way to the real object — for instance, caching results or logging calls for auditing purposes.
  • Interface Transparency: Because the proxy implements the same interface as the real subject, the client doesn’t need to change. Using the proxy feels just like using the real object.

In short, a Proxy acts as a middleman that can introduce controls or optimizations without modifying the real object’s code. Next, let’s use a real-world analogy to solidify this concept.

Real-World Analogy

Imagine a busy executive with a personal assistant. The assistant acts as a gatekeeper for the executive: they filter calls and meeting requests, handle trivial matters, and only forward important things to the boss. In this analogy, the assistant is a proxy for the executive. People interact with the assistant just as they would with the executive (asking for appointments or information), but the assistant controls when and how the executive actually gets involved. Unimportant requests might never reach the boss at all. Similarly, in software, a Proxy object stands in front of the real object (the “boss”) and intercepts requests, allowing only the appropriate ones through or adding some pre/post processing. The real object can focus on its core work, while the proxy handles the access logistics.

Proxy vs Decorator vs Adapter

It’s easy to mix up Proxy with the Decorator or Adapter patterns since all three involve one object wrapping another. Here’s how to distinguish them:

Proxy vs Decorator

Both proxies and decorators implement the same interface as the object they wrap. The difference is in intent. A Proxy controls access to the object (for purposes like security, lazy loading, etc.) and usually doesn’t modify the actual results. A Decorator also wraps an object but its goal is to add new behavior or enhancements to the object’s output or capabilities. Decorators are often chained together to layer multiple enhancements, whereas you typically use a single proxy for a particular purpose.

Proxy vs Adapter

An Adapter is meant to convert one interface to another to satisfy client expectations, whereas a Proxy maintains the same interface as the object it represents. In other words, use Adapter when you need to make two incompatible interfaces work together; use Proxy when you need to insert an intermediary for additional control but the interface of the original object is already what the client expects.

Practical Applications of Proxy

The Proxy pattern is quite versatile. Some common real-world applications include:

  • Lazy Loading: Using a proxy to load or instantiate objects on demand. For example, a large image file might not be loaded into memory until you actually try to display it — a proxy can stand in for the image and load it at the last moment.
  • Access Control: Using a proxy to protect access to a sensitive object. For instance, a proxy might ensure a user has the correct permissions (or a valid API key) before allowing calls to a secure service.
  • Remote Access and Others: Using a proxy to represent an object that lives in a different address space or on a remote server. The proxy handles the network communication, so the local code can use the remote object as if it were local. Proxies are also used for caching (to store results of expensive operations) and logging (to record or monitor usage). In these cases, the proxy interposes additional functionality without changing the client or the real subject.

Implementation Challenges and Tips

Implementing a Proxy is usually straightforward, but a few challenges can arise:

  • Boilerplate Code: Because a proxy class often mirrors the interface of the real class, you may end up writing a lot of pass-through methods. If the real subject’s interface changes, the proxy needs updating too. Some languages offer dynamic proxy generation or reflection to reduce this boilerplate. In any case, it’s important to keep the proxy’s interface in sync with the real subject.
  • Performance Overhead: Every call goes through an extra layer, so there’s a minor overhead. In most scenarios this overhead is negligible, but if you’re proxying something very simple or frequently called, it might be unnecessary overhead. Avoid using a proxy in performance-critical hotspots unless it’s absolutely needed.
  • Ensuring No Bypass: Make sure clients can’t skip the proxy and access the real object directly. If some code calls the real object directly while other code goes through the proxy, you could get inconsistent behavior. To prevent this, the real object is often kept private or inaccessible except via the proxy. For example, you might only expose a factory that gives out proxied instances and hide the real constructor.

By anticipating these issues, you can use Proxy effectively. The pattern is well-supported in many languages and frameworks (for example, proxy objects are commonly used in ORMs, network stubs, and more), but it’s up to you to ensure the added layer remains transparent and correct.

Pros, Cons, and Limitations

Pros

The Proxy pattern lets you introduce extra behavior (access control, lazy loading, caching, logging, etc.) without modifying the original classes. It can improve performance in cases where you avoid unnecessary work (loading data only when needed, or returning cached results). It also provides a clear separation of concerns — the real object handles core functionality, while the proxy handles ancillary tasks like security or network communication. This can make your code cleaner and more maintainable.

Cons

A proxy adds an extra layer of indirection, which means additional complexity and a slight performance cost on each call. For simple or high-speed operations, this added complexity might be overkill. Having more classes (the proxy class and potentially related infrastructure) also means more code to maintain. If team members or other code accidentally bypass the proxy, it can undermine the proxy’s purpose. Overusing proxies (adding them everywhere without good reason) can lead to a convoluted design with little benefit. And while a proxy can postpone or reduce work, it doesn’t eliminate the cost of that work entirely — for example, delaying an expensive operation is helpful only if it turns out you don’t need to do it at all, otherwise the cost still occurs later.

Example: Proxy for Security Checks in Dart

Let’s look at a simple example in Dart. Suppose we have a data source that provides sensitive information. We want only authorized clients to get that data. We can create a proxy that will check a secret key before delegating to the real data source.

First, here’s the interface and the real data source class:

// The interface that both the real service and the proxy will implement.
abstract class DataSource {
String fetchData();
}

// The real object that provides the actual data.
class RealDataSource implements DataSource {
@override
String fetchData() {
return "Sensitive data: [Classified Info]";
}
}

Now, we implement a proxy that wraps a RealDataSource and only allows access if the correct key is provided:

// The proxy that controls access to RealDataSource.
class SecureDataSourceProxy implements DataSource {
final DataSource _realSource;
final String _providedKey;

SecureDataSourceProxy(this._realSource, this._providedKey);

static const String _AUTHORIZED_KEY = "SECRET123"; // required key for access

@override
String fetchData() {
if (_providedKey == _AUTHORIZED_KEY) {
// Access granted: forward to the real object
return _realSource.fetchData();
} else {
// Access denied: do not call the real object
return "Access denied: invalid access key.";
}
}
} SecureDataSourceProxy(this._realSource, this._providedKey);

Using the proxy is straightforward. We pass the real data source and a key to the SecureDataSourceProxy, then use it like a normal DataSource:

void main() {
DataSource realService = RealDataSource();

// Client with the wrong key
DataSource proxy1 = SecureDataSourceProxy(realService, "WRONG_KEY");
print(proxy1.fetchData()); // prints: Access denied: invalid access key.

// Client with the correct key
DataSource proxy2 = SecureDataSourceProxy(realService, "SECRET123");
print(proxy2.fetchData()); // prints: Sensitive data: [Classified Info]
}

In this example, the SecureDataSourceProxy acts as a guard. The client code calls fetchData() on a DataSource without caring whether it's the proxy or the real service. The proxy intercepts the call, checks _providedKey, and only if the key is correct does it delegate to the real RealDataSource. If the key is wrong, it returns an error message and the real object is never touched. This way, we've added a security layer around RealDataSource without changing its code at all.

Conclusion

The Proxy design pattern is a handy tool for situations where you need an intermediary to manage access to an object. By inserting this extra layer, you can perform actions like authentication, lazy loading, or caching in a centralized way. As we’ve seen, proxies can make systems more flexible and secure by decoupling these concerns from the core logic.

What do you think about the Proxy pattern? Have you used it in your projects, or have you seen it misused? I’d love to hear your thoughts or stories in the comments.

If you found this article helpful, consider giving it a clap, sharing it with others, and following for more articles on software design patterns and best practices. Let’s discuss — do you have a Proxy pattern experience or opinion to share?

--

--

Maxim Gorin
Maxim Gorin

Written by Maxim Gorin

Team lead in mobile development with a passion for Fintech and Flutter. Sharing insights and stories from the tech and dev world on this blog.

Responses (1)