Skip to content
StackPractices
intermediate By Mathias Paulenko

Proxy Pattern

Provide a surrogate or placeholder for another object to control access to it. A structural design pattern for access control, lazy loading, and logging.

Topics: design

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Proxy Pattern

Overview

The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object. The proxy controls access to the real subject, adding a layer of indirection that can be used for lazy loading, access control, caching, logging, or monitoring — without changing the subject’s code.

When to Use

Use the Proxy Pattern when:

  • You need lazy initialization for expensive objects (create on first use)
  • You want to control access rights to an object (authentication, authorization)
  • You need to cache results of expensive operations
  • You want to log or monitor calls to an object transparently
  • The real object is remote and you need a local representative

Solution

Python

from abc import ABC, abstractmethod

class Image(ABC):
    @abstractmethod
    def display(self):
        pass

class RealImage(Image):
    def __init__(self, filename: str):
        self.filename = filename
        self._load_from_disk()

    def _load_from_disk(self):
        print(f"Loading image: {self.filename}")

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

class ImageProxy(Image):
    def __init__(self, filename: str):
        self.filename = filename
        self._real_image = None

    def display(self):
        if self._real_image is None:
            self._real_image = RealImage(self.filename)
        self._real_image.display()

# Usage: expensive object is not loaded until needed
proxy = ImageProxy("photo.jpg")  # No loading yet
proxy.display()                   # Loads and displays
proxy.display()                   # Uses cached RealImage

JavaScript

class RealImage {
  constructor(filename) {
    this.filename = filename;
    this.loadFromDisk();
  }

  loadFromDisk() {
    console.log(`Loading image: ${this.filename}`);
  }

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

class ImageProxy {
  constructor(filename) {
    this.filename = filename;
    this.realImage = null;
  }

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

// Usage
const proxy = new ImageProxy("photo.jpg");
proxy.display(); // Lazy loads
proxy.display(); // Uses cached instance

Java

public interface Image {
    void display();
}

public class RealImage implements Image {
    private final String filename;

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

    private void loadFromDisk() {
        System.out.println("Loading image: " + filename);
    }

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

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

    public ImageProxy(String filename) {
        this.filename = filename;
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}

// Usage
Image proxy = new ImageProxy("photo.jpg");
proxy.display(); // Lazy loads
proxy.display(); // Reuses cached RealImage

Explanation

The Proxy Pattern involves three roles:

  • Subject Interface (Image): The common interface shared by both the real object and the proxy
  • Real Subject (RealImage): The actual object that performs the real work
  • Proxy (ImageProxy): Controls access to the real subject, adding behavior before or after forwarding requests

The proxy can intercept operations to add caching, logging, access control, or lazy initialization transparently.

Variants

VariantPurposeExample
Virtual ProxyLazy initializationLoading large images on demand
Protection ProxyAccess controlChecking permissions before method execution
Caching ProxyMemoizationCaching API responses or computed results
Remote ProxyNetwork transparencyLocal stub for a remote service
Smart ReferenceReference countingTracking object usage for cleanup

What Works

  • Keep the proxy interface identical to the real subject — clients should not know they are using a proxy
  • Use lazy initialization only when the real object is expensive — otherwise, the proxy adds unnecessary complexity
  • Handle thread safety in caching proxies when multiple clients may access shared cached data
  • Implement reference counting in smart proxies to manage lifecycle of expensive resources
  • Document proxy behavior (e.g., “this proxy caches results for 5 minutes”) so callers understand performance characteristics

Common Mistakes

  • Exposing the proxy’s internal state or letting clients bypass it to reach the real subject directly
  • Using a proxy when a simple decorator or direct reference would suffice, adding unnecessary indirection
  • Forgetting to handle exceptions in the proxy, letting failures silently bypass logging or cleanup logic
  • Implementing caching proxies without cache invalidation, leading to stale data
  • Not synchronizing access in multi-threaded environments, causing race conditions in lazy initialization

Frequently Asked Questions

Q: What is the difference between Proxy and Decorator? A: Both wrap objects and add behavior. Proxy controls access to the wrapped object (often for structural reasons like lazy loading or remote access). See Caching Proxy for a concrete example. Decorator adds responsibilities dynamically, usually for functional enhancement. The intent differs even if the structure looks similar.

Q: Can a proxy wrap another proxy? A: Yes. You can stack proxies — for example, a caching proxy wrapping a remote proxy. Each layer adds its own behavior. Keep the stack shallow to avoid confusing stack traces and performance overhead.

Q: When should I use a Proxy instead of a Factory? A: Use a Factory when you want to control which class is instantiated. Use a Proxy when you want to control access to an already-instantiated object or delay its creation.

Is this pattern suitable for small projects?

For small projects with few components, this pattern may add unnecessary complexity. Start simple and introduce the pattern when you feel the pain it solves.

How does this pattern compare to alternatives?

Each pattern makes different trade-offs. Review the variants table above and consider your specific constraints: team size, performance requirements, and future scaling plans.

Can I partially apply this pattern?

Yes. Many teams adopt patterns incrementally. Start with the core idea and add sophistication as needed. The pattern is a guide, not a strict blueprint.

Advanced Topics

Scenario: Proxy for Cache and Access Control

// Proxy pattern: control access to an object
interface DataService {
  getData(key: string): Promise<unknown>;
  setData(key: string, value: unknown): Promise<void>;
}

// Real subject: service that reads from DB
class DatabaseService implements DataService {
  constructor(private db: Pool) {}
  async getData(key: string): Promise<unknown> {
    const res = await this.db.query("SELECT value FROM cache WHERE key = $1", [key]);
    return res.rows[0]?.value || null;
  }
  async setData(key: string, value: unknown): Promise<void> {
    await this.db.query("INSERT INTO cache (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2", [key, JSON.stringify(value)]);
  }
}

// Proxy 1: Cache
class CacheProxy implements DataService {
  private cache = new Map<string, { value: unknown; expiry: number }>();
  constructor(private real: DataService, private ttlMs: number = 60000) {}
  async getData(key: string): Promise<unknown> {
    const cached = this.cache.get(key);
    if (cached && cached.expiry > Date.now()) return cached.value;
    const value = await this.real.getData(key);
    this.cache.set(key, { value, expiry: Date.now() + this.ttlMs });
    return value;
  }
  async setData(key: string, value: unknown): Promise<void> {
    await this.real.setData(key, value);
    this.cache.delete(key); // invalidate cache on write
  }
}

// Proxy 2: Access control
class AccessProxy implements DataService {
  constructor(private real: DataService, private userRole: string) {}
  async getData(key: string): Promise<unknown> {
    if (this.userRole !== "admin" && this.userRole !== "reader") {
      throw new Error("Access denied: insufficient permissions");
    }
    return this.real.getData(key);
  }
  async setData(key: string, value: unknown): Promise<void> {
    if (this.userRole !== "admin") {
      throw new Error("Access denied: write requires admin");
    }
    await this.real.setData(key, value);
  }
}

// Proxy 3: Logging
class LoggingProxy implements DataService {
  constructor(private real: DataService) {}
  async getData(key: string): Promise<unknown> {
    const start = Date.now();
    const result = await this.real.getData(key);
    console.log(`[PROXY] getData(${key}) ${Date.now() - start}ms`);
    return result;
  }
  async setData(key: string, value: unknown): Promise<void> {
    await this.real.setData(key, value);
    console.log(`[PROXY] setData(${key})`);
  }
}

// Composition: Logging -> Access -> Cache -> DB
const service = new LoggingProxy(
  new AccessProxy(
    new CacheProxy(
      new DatabaseService(dbPool)
    ),
    currentUser.role
  )
);

Lessons:

  • Proxy controls access to the real object without changing its interface
  • Types: virtual (lazy load), protection (access control), remote (RPC), smart (cache)
  • Proxies compose: logging + access + cache + DB
  • The client does not know it is talking to a proxy
  • Proxy vs Decorator: Proxy controls access; Decorator adds behavior

### Proxy vs Decorator: which do I use?

Proxy controls access to the object: decides if, when, and how to access. Decorator adds behavior: always delegates to the real object. Proxy decides (access, cache, lazy); Decorator enriches (logging, metrics, retry). Proxy does not add new functionality: it only controls. Decorator adds. Use Proxy for access control, cache, lazy loading. Use Decorator for logging, metrics, additional validation.