Proxy Pattern

advanced 4-7 YOE lld design-pattern structural

The Proxy pattern puts a surrogate object in front of the real object to control access to it. The proxy has the same interface as the real object, so clients don’t even know they’re talking to a proxy.

Think of it like a security guard at a building entrance. We don’t interact with the building directly. The guard checks our ID, decides if we can enter, maybe logs our visit — all before letting us through to the actual building.

The Problem It Solves

Sometimes we can’t or don’t want to give clients direct access to an object:

  • The object is expensive to create and we want to delay it until it’s actually needed (lazy loading)
  • We need to check permissions before allowing access
  • We want to cache results to avoid repeated expensive operations
  • We need to log every access to the object

A proxy handles all of this without the client knowing anything changed.

Types of Proxy

Three Common Proxy Types
Virtual Proxy
Delays creating the real object until we actually use it. Great for expensive resources like large images or DB connections.
Protection Proxy
Checks if the client has permission before forwarding the request. Like an access control layer.
Caching Proxy
Stores results of expensive operations and returns the cached version if the same request comes again.

Virtual Proxy — Lazy Image Loading

The image isn’t loaded until we actually call display(). This saves memory and startup time.

from abc import ABC, abstractmethod

# Common interface
class Image(ABC):
    @abstractmethod
    def display(self) -> None:
        pass

# Real image -- expensive to create (loads from disk)
class RealImage(Image):
    def __init__(self, filename: str):
        self.filename = filename
        self._load_from_disk()  # Slow! Happens at creation time

    def _load_from_disk(self):
        print(f"Loading {self.filename} from disk... (slow)")

    def display(self):
        print(f"Displaying {self.filename}")

# Proxy -- delays loading until display() is called
class ImageProxy(Image):
    def __init__(self, filename: str):
        self.filename = filename
        self._real_image = None  # not loaded yet!

    def display(self):
        if self._real_image is None:
            self._real_image = RealImage(self.filename)  # load on first use
        self._real_image.display()

# Without proxy: image loads immediately (even if never displayed)
# With proxy: image loads ONLY when display() is called
gallery = [ImageProxy(f"photo_{i}.jpg") for i in range(100)]
# Nothing loaded yet! Memory is clean.

gallery[0].display()  # NOW photo_0.jpg loads
gallery[0].display()  # Already loaded, just displays
// Real image -- expensive to create
class RealImage {
  constructor(filename) {
    this.filename = filename;
    this.#loadFromDisk();
  }

  #loadFromDisk() {
    console.log(`Loading ${this.filename} from disk... (slow)`);
  }

  display() {
    console.log(`Displaying ${this.filename}`);
  }
}

// Proxy -- delays loading
class ImageProxy {
  #filename;
  #realImage = null;

  constructor(filename) {
    this.#filename = filename;
  }

  display() {
    if (!this.#realImage) {
      this.#realImage = new RealImage(this.#filename);
    }
    this.#realImage.display();
  }
}

// Create 100 proxies -- nothing loads
const gallery = Array.from({ length: 100 },
  (_, i) => new ImageProxy(`photo_${i}.jpg`)
);

gallery[0].display(); // NOW it loads
gallery[0].display(); // Already loaded
interface Image {
    void display();
}

class RealImage implements Image {
    private String filename;

    RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("Loading " + filename + " from disk... (slow)");
    }

    public void display() {
        System.out.println("Displaying " + filename);
    }
}

class ImageProxy implements Image {
    private String filename;
    private RealImage realImage;

    ImageProxy(String filename) {
        this.filename = filename;
        // realImage is null -- not loaded yet
    }

    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename); // load on first use
        }
        realImage.display();
    }
}

Protection Proxy — Access Control

Only users with the right role can perform certain actions.

class Document:
    def __init__(self, content: str):
        self.content = content

    def read(self) -> str:
        return self.content

    def write(self, content: str):
        self.content = content

class DocumentProxy:
    def __init__(self, document: Document, user_role: str):
        self._document = document
        self._user_role = user_role

    def read(self) -> str:
        # Everyone can read
        return self._document.read()

    def write(self, content: str):
        # Only admins can write
        if self._user_role != "admin":
            raise PermissionError("Only admins can edit this document")
        self._document.write(content)

doc = Document("Secret stuff")
viewer = DocumentProxy(doc, "viewer")
admin = DocumentProxy(doc, "admin")

print(viewer.read())       # "Secret stuff" -- works
admin.write("Updated")     # works
# viewer.write("Hacked!")  # raises PermissionError
class Document {
  constructor(content) { this.content = content; }
  read() { return this.content; }
  write(content) { this.content = content; }
}

class DocumentProxy {
  #document;
  #userRole;

  constructor(document, userRole) {
    this.#document = document;
    this.#userRole = userRole;
  }

  read() {
    return this.#document.read();
  }

  write(content) {
    if (this.#userRole !== "admin") {
      throw new Error("Only admins can edit this document");
    }
    this.#document.write(content);
  }
}

const doc = new Document("Secret stuff");
const viewer = new DocumentProxy(doc, "viewer");
const admin = new DocumentProxy(doc, "admin");

console.log(viewer.read());     // "Secret stuff"
admin.write("Updated");         // works
// viewer.write("Hacked!");     // throws Error
class Document {
    private String content;
    Document(String content) { this.content = content; }
    String read() { return content; }
    void write(String content) { this.content = content; }
}

class DocumentProxy {
    private Document document;
    private String userRole;

    DocumentProxy(Document document, String userRole) {
        this.document = document;
        this.userRole = userRole;
    }

    String read() {
        return document.read();
    }

    void write(String content) {
        if (!"admin".equals(userRole)) {
            throw new SecurityException("Only admins can edit");
        }
        document.write(content);
    }
}

Proxy vs Decorator vs Adapter

These three look similar because they all wrap objects. Here’s how to tell them apart:

PatternPurposeKey Difference
ProxyControl access to an objectSame interface, manages lifecycle/access
DecoratorAdd new behavior to an objectSame interface, adds functionality
AdapterMake incompatible interfaces workDifferent interface, translates

The easiest way to remember: Proxy controls, Decorator enhances, Adapter translates.

When to Use

  • Lazy initialization — delay expensive object creation until needed
  • Access control — check permissions before forwarding requests
  • Caching — store expensive results and reuse them
  • Logging/monitoring — track every access to an object
  • Remote proxy — represent an object that lives on a different server (like RPC stubs)

When NOT to Use

  • When there’s no reason to control access — adding a proxy “just in case” is overengineering
  • When the overhead of an extra layer hurts performance more than it helps
  • When the real object is cheap to create and has no access restrictions

In simple language, a Proxy is a bodyguard standing in front of the real object. It looks just like the real thing from the outside (same interface), but it decides when, how, and if we get access. Lazy loading, permission checks, caching — the proxy handles all of it, and the client never knows it’s not talking to the real thing.