Proxy Pattern
- Description: Surrogate object exposing the same interface as a real subject to control access — used for lazy initialization, remote calls, permission checks, smart references, and caching.
- My Notion Note ID: K2C-2-12
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Intent
- 2. Structure
- 3. Proxy Variants
- 4. C++ Example
- 5. Smart Pointers as Proxies
- 6. When to Use
- 7. Pitfalls
- 8. Related Patterns
- 9. References
1. Intent
- A stand-in for another object that controls access to it.
- Same interface as the real subject → client can't tell (and shouldn't need to).
- Reason to intercept varies → lazy load, remote location, permission check, ref counting, copy-on-write, caching.
The "what" (interface) is unchanged. The "when / whether / where" of the actual call is the proxy's job.
2. Structure
| Role | Responsibility |
|---|---|
| Subject | Common interface for RealSubject and Proxy. |
| RealSubject | Actual object doing the real work. |
| Proxy | Implements Subject. Holds a reference (or way to obtain one) to RealSubject. Adds pre/post logic around calls. |
| Client | Uses Subject* polymorphically. |
3. Proxy Variants
| Variant | Intercepts for | Typical use |
|---|---|---|
| Virtual proxy | Lazy creation of expensive real subject | Don't load a 50 MB image until needed |
| Remote proxy | Cross-process / network call | RPC stub, gRPC client class |
| Protection proxy | Authorization | Check permissions before delegating |
| Smart-reference proxy | Lifetime, locking, accounting | shared_ptr, mutex auto-lock, ref counts |
| Caching proxy | Memoization | Skip the real call when the answer is cached |
| Logging / firewall proxy | Observability, rate limiting | Count calls, throttle, log requests |
| Copy-on-write proxy | Shared until mutation | Old std::string SSO COW variants, OS page-level COW |
Multiple roles can be combined in one proxy (caching + remote = remote-with-local-cache).
4. C++ Example
4.1 Virtual proxy — lazy image loading
#include <memory>
#include <string>
#include <iostream>
#include <utility>
class Image {
public:
virtual ~Image() = default;
virtual void display() = 0;
};
class RealImage : public Image {
public:
explicit RealImage(std::string path) : path_(std::move(path)) {
std::cout << "loading " << path_ << " (slow)\n";
// pretend megabytes of decode happen here
}
void display() override { std::cout << "draw " << path_ << "\n"; }
private:
std::string path_;
};
class ImageProxy : public Image {
public:
explicit ImageProxy(std::string path) : path_(std::move(path)) {}
void display() override {
if (!real_) real_ = std::make_unique<RealImage>(path_);
real_->display();
}
private:
std::string path_;
std::unique_ptr<RealImage> real_; // created on first use
};
int main() {
ImageProxy img{"hero.png"};
std::cout << "before display\n";
img.display(); // loads + draws
img.display(); // just draws
}
4.2 Protection proxy
class FileService {
public:
virtual ~FileService() = default;
virtual std::string read(const std::string& path) = 0;
};
class RealFileService : public FileService {
public:
std::string read(const std::string& path) override {
return "<contents of " + path + ">";
}
};
class AuthFileProxy : public FileService {
public:
AuthFileProxy(std::shared_ptr<FileService> real, std::string role)
: real_(std::move(real)), role_(std::move(role)) {}
std::string read(const std::string& path) override {
if (role_ != "admin" && path.starts_with("/etc/"))
throw std::runtime_error("forbidden");
return real_->read(path);
}
private:
std::shared_ptr<FileService> real_;
std::string role_;
};
4.3 Caching proxy
#include <unordered_map>
class ExpensiveCompute {
public:
virtual ~ExpensiveCompute() = default;
virtual int compute(int n) = 0;
};
class CachingProxy : public ExpensiveCompute {
public:
explicit CachingProxy(std::shared_ptr<ExpensiveCompute> inner)
: inner_(std::move(inner)) {}
int compute(int n) override {
if (auto it = cache_.find(n); it != cache_.end()) return it->second;
int r = inner_->compute(n);
cache_.emplace(n, r);
return r;
}
private:
std::shared_ptr<ExpensiveCompute> inner_;
std::unordered_map<int, int> cache_;
};
5. Smart Pointers as Proxies
C++ smart pointers are textbook smart-reference proxies — operator-> and operator* forward to the pointee while the proxy manages lifetime / counting / locking.
std::unique_ptr<T>— owns, deletes on destruction. No interface change visible to the user —p->m()and(*p).m()look exactly like raw-pointer access.std::shared_ptr<T>— owns + reference-counts.std::weak_ptr<T>— non-owning observer; protects against dangling.std::lock_guard/std::unique_lockover a wrapped mutex+T — accessor proxy that locks for the duration of the access. Standard pattern:
template <class T>
class LockedAccess {
public:
LockedAccess(std::mutex& m, T& obj) : lk_(m), obj_(obj) {}
T* operator->() { return &obj_; }
T& operator*() { return obj_; }
private:
std::scoped_lock<std::mutex> lk_;
T& obj_;
};
auto a = guard.lock(); a->method(); — lock held only for the access window.
std::expected<T, E>::operator->— proxy access into the success branch.
6. When to Use
Use when:
- Construction of the real object is expensive and may not be needed → virtual proxy.
- Real object lives elsewhere (process, machine) → remote proxy.
- Access requires authorization → protection proxy.
- Want to attach lifetime / sync / counting transparently → smart-reference proxy (smart pointers).
- Want to cache results without changing the caller's code → caching proxy.
Avoid when:
- You don't need to intercept anything → just use the object.
- Interception changes the interface → that's an Adapter or a Facade, not a Proxy.
- Adds behavior the client actively wants → that's a Decorator.
7. Pitfalls
- Hidden cost. A proxy method call may transparently trigger network I/O, disk load, lock contention. Document this — surprise latency in "ordinary" calls.
- Equality / identity.
proxy == real_subjectandproxy == another_proxyrarely behave as users expect. Override carefully or document. - Lifetime cycles. Caching proxy holds
shared_ptrto subject; subject holdsshared_ptrback → leak. Useweak_ptrfor back-references. - Thread safety. Lazy init in a virtual proxy needs
call_onceor atomic check; not all proxies are safe for concurrent first-use. - Cache invalidation. Caching proxy → "two hard things in CS". Stale results cost more than the proxy saves.
- Remote-proxy failure modes. Network call can throw, time out, partial-write. The local-looking method call hides these. Make the failure mode visible in the interface (exceptions,
expected). - Authorization drift. Protection proxy gates one entry point; if the real subject is reachable some other way (a friend, a back-door API), the gate is meaningless. Make the subject inaccessible except through the proxy.
- Proliferation. Every method needs forwarding boilerplate. Consider templates / macros / code-gen for large interfaces.
8. Related Patterns
- Proxy vs Decorator. Same mechanics (wrap + forward), different intent. Decorator adds responsibilities the client wants — composable, stackable. Proxy controls access — usually invisible, "you didn't ask for caching, you got it". Decorators chain naturally; proxies don't typically stack.
- Proxy vs Adapter. Adapter changes the interface. Proxy preserves it.
- Proxy vs Facade. Facade introduces a new interface over a subsystem of many classes. Proxy preserves the subject's interface to one object.
- Proxy vs Bridge. Bridge is a structural separation set up at design time. Proxy is a stand-in at run time for the same interface.
- Proxy + Flyweight. Flyweights are often handed out via a proxy that keeps the extrinsic state.
- Remote Proxy vs Stub/Skeleton (RPC). Stub is the client-side proxy; skeleton is the server-side dispatcher. Stub uses the Proxy pattern; gRPC, CORBA, COM all instantiate this idea.
9. References
- GoF — Design Patterns, Proxy ch.
- refactoring.guru — Proxy
- sourcemaking — Proxy
- cppreference —
std::unique_ptr,std::shared_ptr,std::weak_ptr - POSA1 — Pattern-Oriented Software Architecture, Vol. 1 — Broker / Remote Proxy