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

  • 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_lock over 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_subject and proxy == another_proxy rarely behave as users expect. Override carefully or document.
  • Lifetime cycles. Caching proxy holds shared_ptr to subject; subject holds shared_ptr back → leak. Use weak_ptr for back-references.
  • Thread safety. Lazy init in a virtual proxy needs call_once or 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