Structural Pattern

Proxy

Provides a surrogate or placeholder for another object to control access to it.

Proxy Pattern: A Complete Guide with Examples in 11 Programming Languages

The Proxy Pattern is a structural design pattern that provides a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform operations before or after a request reaches the target — without the client knowing it is dealing with a proxy at all. Whether you need lazy initialization, access control, request logging, caching, remote object representation, or smart reference counting, the Proxy Pattern gives you a transparent layer of indirection that keeps your core object clean and your cross-cutting concerns organized.

In this comprehensive guide, we'll explore the Proxy Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.

Table of Contents

What is the Proxy Pattern?

The Proxy Pattern places an intermediary object in front of a real object, sharing the same interface. Clients talk to the proxy thinking they're talking to the real object. The proxy decides whether and how to forward requests to the real object, and can add behavior before or after forwarding.

Think of it like a personal assistant. When people want to reach a busy executive, they go through the assistant first. The assistant filters calls, schedules meetings, and handles routine requests — sometimes without ever involving the executive. The assistant is the proxy.

Why Use the Proxy Pattern?

The Proxy Pattern offers several compelling benefits:

  1. Lazy Initialization (Virtual Proxy): Delay creating a heavy object until it is actually needed
  2. Access Control (Protection Proxy): Check permissions before forwarding a request
  3. Caching (Caching Proxy): Cache results of expensive operations and return cached values on repeat calls
  4. Logging and Auditing: Record what operations are performed and by whom
  5. Remote Access (Remote Proxy): Represent an object that lives in another process or machine
  6. Smart References: Perform additional actions when an object is accessed (e.g., reference counting, null checks)
  7. Rate Limiting: Throttle how frequently the real object can be called

Proxy Pattern Comparison

Let's compare Proxy with related structural patterns:

PatternPurposeInterfaceAwarenessUse Case
ProxyControl access to real objectSame as real objectClient unaware of proxyLazy init, auth, caching, logging
DecoratorAdd behavior to objectSame as wrapped objectClient may knowExtending functionality dynamically
FacadeSimplify complex subsystemNew simplified interfaceClient uses facade intentionallyReducing complexity for clients
AdapterConvert incompatible interfaceDifferent from adapteeClient uses adapter intentionallyBridging incompatible APIs
FlyweightShare state to save memorySame as shared objectClient may knowMillions of fine-grained objects

Key Distinctions:

  • Proxy vs. Decorator: Both share the interface of the wrapped object, but Proxy controls access, while Decorator adds behavior. A proxy typically manages the lifecycle of its subject; a decorator does not.
  • Proxy vs. Facade: Facade provides a simplified interface over many objects. Proxy provides the same interface over one object.

Proxy Pattern Explained

The Proxy Pattern typically involves:

Core Components:

  1. Subject Interface: Declares the common interface for both the real object and proxy. Clients program against this interface so they can work with either transparently.
  2. Real Subject: The actual object that does the real work. The proxy delegates to it.
  3. Proxy: Implements the subject interface and holds a reference to the real subject. Adds its logic before/after delegating.
  4. Client: Works with both the real subject and the proxy through the subject interface — ideally without knowing which one it has.

Common Proxy Types:

  • Virtual Proxy: Defers creation of an expensive object until first use. The proxy stands in as a lightweight placeholder.
  • Protection Proxy: Controls access based on permissions. Checks whether the caller has the right to perform the requested operation.
  • Caching Proxy: Caches results of expensive calls and returns cached results on subsequent identical requests.
  • Remote Proxy: Represents an object in a different address space (e.g., a gRPC/REST stub, RMI stub).
  • Logging Proxy: Records calls to the real object for auditing, debugging, or analytics.
  • Smart Reference Proxy: Performs cleanup when no references remain (reference counting), or pre-checks before forwarding (null safety, circuit breaking).

Class Diagram

Here's the UML class diagram showing the Proxy structure:

Beginner-Friendly Example

Let's start with a classic Virtual Proxy example: lazy-loading a large image. Loading a high-resolution image from disk is expensive. A ImageProxy stands in for the real image, loading it only when it actually needs to be displayed.

from abc import ABC, abstractmethod
import time


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

    @abstractmethod
    def get_filename(self) -> str:
        pass


# Real Subject — expensive to load
class HighResolutionImage(Image):
    def __init__(self, filename: str) -> None:
        self._filename = filename
        self._load_from_disk()

    def _load_from_disk(self) -> None:
        print(f"[RealImage] Loading '{self._filename}' from disk... (expensive operation)")
        time.sleep(0.1)  # Simulate I/O

    def display(self) -> None:
        print(f"[RealImage] Displaying '{self._filename}'")

    def get_filename(self) -> str:
        return self._filename


# Virtual Proxy — defers loading until display() is called
class ImageProxy(Image):
    def __init__(self, filename: str) -> None:
        self._filename = filename
        self._real_image: HighResolutionImage | None = None

    def display(self) -> None:
        if self._real_image is None:
            print(f"[Proxy] First access — initializing real image...")
            self._real_image = HighResolutionImage(self._filename)
        self._real_image.display()

    def get_filename(self) -> str:
        return self._filename  # Can serve this without loading the real image


# Client code
if __name__ == "__main__":
    print("=== Creating image proxies (no loading yet) ===")
    images = [
        ImageProxy("photo_vacation.jpg"),
        ImageProxy("photo_wedding.jpg"),
        ImageProxy("photo_portrait.jpg"),
    ]

    print("\n=== Accessing filenames (no loading) ===")
    for img in images:
        print(f"  File: {img.get_filename()}")

    print("\n=== Displaying first image (loads now) ===")
    images[0].display()

    print("\n=== Displaying first image again (already loaded) ===")
    images[0].display()

    print("\n=== Displaying second image (loads now) ===")
    images[1].display()

Production-Ready Example

Now let's tackle a realistic scenario: a service layer with multiple proxy concerns — caching, access control, and logging — applied to a database-backed user repository. This demonstrates how proxies can be layered (chained) to compose cross-cutting behavior cleanly.

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, Dict, List
import time
import hashlib


@dataclass
class User:
    id: int
    name: str
    email: str
    role: str = "user"


@dataclass
class RequestContext:
    user_id: int
    role: str
    request_id: str = field(default_factory=lambda: hashlib.md5(
        str(time.time()).encode()).hexdigest()[:8])


# Subject Interface
class UserRepository(ABC):
    @abstractmethod
    def find_by_id(self, user_id: int) -> Optional[User]:
        pass

    @abstractmethod
    def find_all(self) -> List[User]:
        pass

    @abstractmethod
    def save(self, user: User) -> User:
        pass

    @abstractmethod
    def delete(self, user_id: int) -> bool:
        pass


# Real Subject — actual database operations
class DatabaseUserRepository(UserRepository):
    def __init__(self):
        # Simulated database
        self._db: Dict[int, User] = {
            1: User(1, "Alice",   "alice@example.com",   "admin"),
            2: User(2, "Bob",     "bob@example.com",     "user"),
            3: User(3, "Charlie", "charlie@example.com", "user"),
        }
        self._next_id = 4

    def find_by_id(self, user_id: int) -> Optional[User]:
        print(f"  [DB] SELECT * FROM users WHERE id = {user_id}")
        time.sleep(0.05)  # Simulate DB latency
        return self._db.get(user_id)

    def find_all(self) -> List[User]:
        print("  [DB] SELECT * FROM users")
        time.sleep(0.1)
        return list(self._db.values())

    def save(self, user: User) -> User:
        if user.id == 0:
            user.id = self._next_id
            self._next_id += 1
        print(f"  [DB] UPSERT user id={user.id}")
        self._db[user.id] = user
        return user

    def delete(self, user_id: int) -> bool:
        print(f"  [DB] DELETE FROM users WHERE id = {user_id}")
        return self._db.pop(user_id, None) is not None


# --- Proxy 1: Caching Proxy ---
class CachingUserRepository(UserRepository):
    def __init__(self, real: UserRepository, ttl_seconds: float = 30.0):
        self._real = real
        self._ttl = ttl_seconds
        self._cache: Dict[str, tuple] = {}  # key -> (value, expiry)

    def _get_cached(self, key: str):
        entry = self._cache.get(key)
        if entry and time.time() < entry[1]:
            print(f"  [Cache] HIT for key='{key}'")
            return entry[0]
        print(f"  [Cache] MISS for key='{key}'")
        return None

    def _set_cached(self, key: str, value) -> None:
        self._cache[key] = (value, time.time() + self._ttl)

    def _invalidate(self, pattern: str) -> None:
        keys = [k for k in self._cache if pattern in k]
        for k in keys:
            del self._cache[k]
        if keys:
            print(f"  [Cache] Invalidated {len(keys)} entries matching '{pattern}'")

    def find_by_id(self, user_id: int) -> Optional[User]:
        key = f"user:{user_id}"
        cached = self._get_cached(key)
        if cached is not None:
            return cached
        result = self._real.find_by_id(user_id)
        if result:
            self._set_cached(key, result)
        return result

    def find_all(self) -> List[User]:
        key = "users:all"
        cached = self._get_cached(key)
        if cached is not None:
            return cached
        result = self._real.find_all()
        self._set_cached(key, result)
        return result

    def save(self, user: User) -> User:
        result = self._real.save(user)
        self._invalidate(f"user:{user.id}")
        self._invalidate("users:all")
        return result

    def delete(self, user_id: int) -> bool:
        result = self._real.delete(user_id)
        self._invalidate(f"user:{user_id}")
        self._invalidate("users:all")
        return result


# --- Proxy 2: Protection Proxy ---
class ProtectedUserRepository(UserRepository):
    def __init__(self, real: UserRepository, context: RequestContext):
        self._real = real
        self._ctx = context

    def _check_role(self, required: str) -> None:
        if self._ctx.role != required:
            raise PermissionError(
                f"Action requires role='{required}', "
                f"but caller has role='{self._ctx.role}'"
            )

    def find_by_id(self, user_id: int) -> Optional[User]:
        return self._real.find_by_id(user_id)

    def find_all(self) -> List[User]:
        return self._real.find_all()

    def save(self, user: User) -> User:
        self._check_role("admin")
        return self._real.save(user)

    def delete(self, user_id: int) -> bool:
        self._check_role("admin")
        return self._real.delete(user_id)


# --- Proxy 3: Logging Proxy ---
class LoggingUserRepository(UserRepository):
    def __init__(self, real: UserRepository, context: RequestContext):
        self._real = real
        self._ctx = context

    def _log(self, action: str, details: str = "") -> None:
        ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
        print(f"  [Audit] {ts} | req={self._ctx.request_id} | "
              f"caller={self._ctx.user_id} | action={action} {details}")

    def find_by_id(self, user_id: int) -> Optional[User]:
        self._log("find_by_id", f"target_id={user_id}")
        result = self._real.find_by_id(user_id)
        self._log("find_by_id", f"result={'found' if result else 'not_found'}")
        return result

    def find_all(self) -> List[User]:
        self._log("find_all")
        result = self._real.find_all()
        self._log("find_all", f"count={len(result)}")
        return result

    def save(self, user: User) -> User:
        self._log("save", f"user_id={user.id} name={user.name}")
        result = self._real.save(user)
        self._log("save", f"success id={result.id}")
        return result

    def delete(self, user_id: int) -> bool:
        self._log("delete", f"target_id={user_id}")
        result = self._real.delete(user_id)
        self._log("delete", f"success={result}")
        return result


# Factory: compose proxy chain
def create_user_repository(context: RequestContext) -> UserRepository:
    """
    Builds the proxy chain:
    Client → Logging → Protection → Caching → Database
    """
    db      = DatabaseUserRepository()
    cached  = CachingUserRepository(db, ttl_seconds=30.0)
    guarded = ProtectedUserRepository(cached, context)
    logged  = LoggingUserRepository(guarded, context)
    return logged


# Client code
if __name__ == "__main__":
    # --- Admin context ---
    admin_ctx = RequestContext(user_id=1, role="admin")
    repo = create_user_repository(admin_ctx)

    print("=== Admin: find user 2 (cache miss) ===")
    user = repo.find_by_id(2)
    print(f"  Result: {user}")

    print("\n=== Admin: find user 2 again (cache hit) ===")
    user = repo.find_by_id(2)
    print(f"  Result: {user}")

    print("\n=== Admin: save new user ===")
    new_user = User(0, "Diana", "diana@example.com", "user")
    saved = repo.save(new_user)
    print(f"  Saved: {saved}")

    print("\n=== Admin: find all (cache miss after save) ===")
    all_users = repo.find_all()
    print(f"  Count: {len(all_users)}")

    # --- Regular user context ---
    print("\n=== Regular user: find by id (allowed) ===")
    user_ctx = RequestContext(user_id=2, role="user")
    user_repo = create_user_repository(user_ctx)
    result = user_repo.find_by_id(3)
    print(f"  Result: {result}")

    print("\n=== Regular user: try to delete (blocked) ===")
    try:
        user_repo.delete(3)
    except PermissionError as e:
        print(f"  Blocked: {e}")

Real-World Use Cases

1. ORM Lazy Loading

ORMs like SQLAlchemy (Python), Hibernate (Java), and Entity Framework (C#) use Virtual Proxies to represent related objects. A User.orders property doesn't load orders from the database until you actually iterate over them.

# SQLAlchemy uses lazy loading proxies transparently
user = session.query(User).get(1)
# orders are NOT loaded yet — user.orders is a proxy
for order in user.orders:  # <-- DB query fires HERE, on first access
    print(order.total)

2. Security / Access Control Layers

In enterprise applications, a Protection Proxy sits between the API controller and the service layer to enforce authorization rules without polluting business logic.

class SecurePaymentService:
    def __init__(self, real: PaymentService, user: AuthUser):
        self._real = real
        self._user = user

    def refund(self, transaction_id: str, amount: float) -> None:
        if not self._user.has_permission("payments:refund"):
            raise PermissionError("Insufficient permissions for refund")
        if amount > 1000 and not self._user.has_permission("payments:large_refund"):
            raise PermissionError("Large refund requires elevated permission")
        self._real.refund(transaction_id, amount)

3. HTTP Client Caching and Retry

A Caching Proxy wraps an HTTP client, returning cached responses for identical GET requests and implementing retry logic for transient failures.

class ResilientHttpClient:
    def __init__(self, real_client, cache_ttl=60, max_retries=3):
        self._client = real_client
        self._cache = {}
        self._cache_ttl = cache_ttl
        self._max_retries = max_retries

    def get(self, url: str) -> Response:
        if url in self._cache:
            entry, expiry = self._cache[url]
            if time.time() < expiry:
                return entry  # Cached response

        for attempt in range(self._max_retries):
            try:
                response = self._client.get(url)
                self._cache[url] = (response, time.time() + self._cache_ttl)
                return response
            except TransientError:
                if attempt == self._max_retries - 1:
                    raise
                time.sleep(2 ** attempt)  # Exponential backoff

4. Remote Proxy (RPC / gRPC Stubs)

When you call a method on a gRPC stub, you are using a Remote Proxy. The stub looks like a local object but marshals arguments, sends them over the network, and unmarshals the response.

# gRPC-generated stub IS a remote proxy
stub = UserServiceStub(grpc.insecure_channel("api.example.com:50051"))
user = stub.GetUser(GetUserRequest(id=42))  # Looks local, runs remotely

5. JavaScript's Built-in Proxy Object

JavaScript has a native Proxy that implements the pattern at the language level, enabling dynamic interception of any property access, assignment, or method call.

const handler = {
    get(target, prop) {
        console.log(`[Proxy] Getting '${prop}'`);
        return prop in target ? target[prop] : `Property '${prop}' not found`;
    },
    set(target, prop, value) {
        if (prop === 'age' && typeof value !== 'number') {
            throw new TypeError('Age must be a number');
        }
        console.log(`[Proxy] Setting '${prop}' = ${value}`);
        target[prop] = value;
        return true;
    }
};

const user = new Proxy({ name: "Alice", age: 30 }, handler);
console.log(user.name);    // [Proxy] Getting 'name' → Alice
user.age = "thirty";       // Throws TypeError

6. Circuit Breaker

A Circuit Breaker is a Proxy that monitors the health of a downstream service. After too many consecutive failures it "opens" the circuit, failing fast without even attempting the real call — protecting the system from cascading failures.

class CircuitBreakerProxy:
    def __init__(self, real_service, failure_threshold=5, recovery_timeout=30):
        self._real = real_service
        self._failures = 0
        self._threshold = failure_threshold
        self._opened_at = None
        self._timeout = recovery_timeout

    @property
    def _is_open(self) -> bool:
        if self._opened_at and time.time() - self._opened_at > self._timeout:
            self._failures = 0  # Try half-open
            self._opened_at = None
            return False
        return self._failures >= self._threshold

    def call(self, *args, **kwargs):
        if self._is_open:
            raise ServiceUnavailableError("Circuit breaker is open")
        try:
            result = self._real.call(*args, **kwargs)
            self._failures = 0
            return result
        except Exception as e:
            self._failures += 1
            if self._failures >= self._threshold:
                self._opened_at = time.time()
            raise

Language-Specific Mistakes and Anti-Patterns

❌ Mistake #1: Breaking the Interface — Proxy Does Not Fully Implement Subject

# BAD: Proxy only implements some methods — clients can't use it interchangeably
class ImageProxy:
    def display(self):  # Only this method — missing get_filename()
        ...

# If someone calls proxy.get_filename(), AttributeError!

# GOOD: Implement every method from the Subject interface
from abc import ABC, abstractmethod

class Image(ABC):
    @abstractmethod
    def display(self) -> None: ...

    @abstractmethod
    def get_filename(self) -> str: ...

class ImageProxy(Image):   # Forces complete implementation
    def display(self) -> None: ...
    def get_filename(self) -> str: ...

❌ Mistake #2: Proxy Adds Business Logic It Shouldn't Own

# BAD: Caching proxy is also validating and transforming data — too many responsibilities
class CachingUserRepository:
    def find_by_id(self, user_id: int):
        if user_id <= 0:             # Validation doesn't belong here
            raise ValueError("...")
        user = self._real.find_by_id(user_id)
        user.name = user.name.title()  # Transformation doesn't belong here either
        return user

# GOOD: Caching proxy only caches; validation lives in domain/service layer
class CachingUserRepository:
    def find_by_id(self, user_id: int):
        cached = self._cache.get(user_id)
        if cached:
            return cached
        result = self._real.find_by_id(user_id)
        self._cache[user_id] = result
        return result

❌ Mistake #3: Not Thread-Safe Lazy Initialization

# BAD: Two threads can both enter the if-block and create duplicate real objects
class ImageProxy:
    def display(self):
        if self._real_image is None:
            self._real_image = HighResolutionImage(self._filename)  # Race condition!
        self._real_image.display()

# GOOD: Use threading.Lock for thread-safe lazy init
import threading

class ImageProxy:
    def __init__(self, filename):
        self._filename = filename
        self._real_image = None
        self._lock = threading.Lock()

    def display(self):
        if self._real_image is None:
            with self._lock:
                if self._real_image is None:  # Double-check inside lock
                    self._real_image = HighResolutionImage(self._filename)
        self._real_image.display()

Frequently Asked Questions

Q1: What is the difference between Proxy and Decorator?

Both patterns wrap an object and share its interface, which is why they're often confused. The key differences are intent and lifecycle:

Proxy controls access to the real subject and usually manages its lifecycle (creates it lazily, destroys it, owns it). The client typically doesn't know it's using a proxy.

Decorator adds behavior to an object. It doesn't control access — it enhances functionality. Decorators are often stacked and the client intentionally wraps objects with them.

A good rule of thumb: if you're asking "should I let this call through?" — that's a Proxy. If you're asking "what extra should I do alongside this call?" — that's a Decorator.

Q2: Can you stack multiple proxies together?

Yes — this is one of the most powerful aspects of the pattern. Because each proxy implements the same interface, proxies can be chained:

# Chain: Logging → Protection → Caching → Database
repo = LoggingProxy(
    ProtectionProxy(
        CachingProxy(DatabaseRepository(), ttl=30),
        context=user_ctx
    ),
    context=user_ctx
)

The order matters: in this chain, every call is logged first, then checked for permissions, then checked in cache, and finally reaches the database if needed.

Q3: Is Python's __getattr__ a form of Proxy?

Yes. Python allows you to intercept attribute access with __getattr__, __getattribute__, and __setattr__, which is effectively a dynamic proxy mechanism built into the language. The unittest.mock.Mock class is a great example — it proxies any attribute or method call dynamically.

class LoggingProxy:
    def __init__(self, real):
        self.__dict__['_real'] = real

    def __getattr__(self, name):
        attr = getattr(self._real, name)
        if callable(attr):
            def wrapper(*args, **kwargs):
                print(f"[Log] Calling {name}")
                return attr(*args, **kwargs)
            return wrapper
        return attr

Q4: What is a Remote Proxy?

A Remote Proxy represents an object that lives in a different process or machine. From the client's perspective, calling a method on it looks the same as calling a local object — the proxy handles all the networking, serialization, and deserialization transparently.

gRPC stubs, WCF proxies, and Java RMI stubs are all Remote Proxies. Modern service meshes (Envoy, Linkerd) act as Remote Proxies at the infrastructure level.

Q5: How does Spring AOP relate to the Proxy Pattern?

Spring AOP (Aspect-Oriented Programming) uses dynamic proxies (either JDK Proxy for interfaces or CGLIB for classes) to intercept method calls and apply cross-cutting concerns like @Transactional, @Cacheable, @Secured, and @Retryable. When you annotate a Spring bean with @Cacheable, Spring wraps it in a Caching Proxy automatically.

@Service
public class UserService {
    @Cacheable("users")          // Spring generates a CachingProxy transparently
    public User findById(Long id) { return repo.findById(id); }

    @Transactional               // Spring generates a TransactionProxy transparently
    public User save(User user)  { return repo.save(user); }
}

Q6: When should I NOT use the Proxy Pattern?

Avoid it when:

  • The indirection adds overhead that outweighs the benefit (e.g., wrapping a trivially cheap operation in a caching proxy).
  • The subject interface is large — implementing every method in every proxy is tedious. Use delegation (by in Kotlin, __getattr__ in Python, dynamic proxies in Java) to reduce boilerplate.
  • You need to modify or extend behavior rather than control access — prefer Decorator.
  • There is only one well-defined behavior to add and it won't change — inline the logic rather than creating a proxy class.

Q7: How do I test code that uses proxies?

Testing becomes easier with proxies, not harder. For unit tests, inject the real subject directly (bypass the proxy). For integration tests, use the full proxy chain. For testing the proxy itself, inject a mock real subject:

from unittest.mock import Mock

def test_caching_proxy_returns_cached_result():
    mock_db = Mock(spec=UserRepository)
    mock_db.find_by_id.return_value = User(1, "Alice", "alice@example.com")

    proxy = CachingUserRepository(mock_db)

    result1 = proxy.find_by_id(1)
    result2 = proxy.find_by_id(1)

    assert result1 == result2
    assert mock_db.find_by_id.call_count == 1  # DB called only once


def test_protection_proxy_blocks_unauthorized():
    mock_db = Mock(spec=UserRepository)
    ctx = RequestContext(user_id=2, role="user")
    proxy = ProtectedUserRepository(mock_db, ctx)

    with pytest.raises(PermissionError):
        proxy.delete(1)

    mock_db.delete.assert_not_called()  # Real delete never reached

Q8: What is a "Smart Reference" proxy?

A Smart Reference Proxy adds automatic behavior when the subject is accessed, beyond simple delegation. Examples include:

  • Reference counting: Track how many clients hold a reference; clean up the subject when count reaches zero
  • Null safety guard: Check if the real subject is still valid before delegating
  • Copy-on-write: Delay copying a shared object until a client tries to modify it
  • Weak reference tracking: Reload the subject if it has been garbage collected

Q9: Is lazy initialization always safe in the Proxy Pattern?

Not without care. In single-threaded code, the simple if null, create pattern is fine. In multi-threaded code, without synchronization, two threads can both observe null and both create the real subject — wasting resources or causing bugs. The standard solution is double-checked locking:

// Thread-safe lazy initialization in Java
public void display() {
    if (realImage == null) {
        synchronized (this) {
            if (realImage == null) {   // Second check inside the lock
                realImage = new HighResolutionImage(filename);
            }
        }
    }
    realImage.display();
}

Q10: How does the Proxy Pattern relate to middleware?

Middleware in web frameworks (Express, Django, ASP.NET) is the Proxy Pattern applied to HTTP request handling. Each middleware component wraps the next one, can intercept the request before passing it along, and can modify the response on the way back. The "request pipeline" is a chain of proxies around the final handler.

# Django middleware — each one is a proxy around the next handler
class LoggingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response  # "real subject"

    def __call__(self, request):
        print(f"[Log] {request.method} {request.path}")
        response = self.get_response(request)   # Delegate
        print(f"[Log] Response: {response.status_code}")
        return response

Key Takeaways

🎯 Core Concept: The Proxy Pattern places an intermediary in front of a real object, implementing the same interface so clients can use proxy and real object interchangeably. The proxy controls access to the real object and can add behavior (caching, logging, auth, lazy init) without the client or the real object knowing.

🔑 Key Benefits:

  • Separation of concerns: Cross-cutting logic (caching, security, logging) lives in proxies, keeping the real subject clean
  • Open/closed principle: Add new behaviors (a new proxy type) without modifying the real subject
  • Transparency: Clients work through the subject interface and don't need to know whether they're talking to a proxy or the real thing
  • Composability: Multiple proxy types can be chained to apply several concerns at once
  • Testability: Each proxy can be tested independently by mocking the real subject

🌐 Language-Specific Highlights:

  • Python: Use ABCs to enforce interface implementation; use threading.Lock with double-checked locking for thread-safe lazy init; __getattr__ enables dynamic proxies
  • TypeScript: Always use async/await consistently across the proxy chain; type the proxy with the interface, not any
  • Java: Use ConcurrentHashMap and synchronized for thread safety; java.lang.reflect.Proxy for dynamic proxies on interfaces; CGLIB/ByteBuddy for class-based dynamic proxies
  • JavaScript: Native Proxy is a language feature (different from the design pattern); explicit class-based proxies are clearer for structural design; avoid caching null results
  • C#: Use record types and immutable models; thread CancellationToken through every async proxy method; consider DispatchProxy for dynamic proxies
  • PHP: Always type-hint interfaces, not concrete classes, to keep proxies swappable
  • Go: Hold an interface reference in the proxy, not an embedded struct; always propagate errors; use sync.RWMutex for concurrent caches
  • Rust: Use Box<dyn Trait> for owned proxy subjects; Arc<Mutex<dyn Trait>> for shared/threaded scenarios; avoid holding &mut references
  • Dart: Use implements, not extends, for proxy classes; Dart's ??= operator simplifies lazy init
  • Swift: Use class (not struct) for proxies with mutable lazy state; use protocols for the subject interface
  • Kotlin: Use by delegation to avoid boilerplate — override only the methods that need proxy logic; use lazy for Virtual Proxies

📋 When to Use Proxy:

  • You need lazy initialization for an expensive object and don't want clients to know
  • You need access control without modifying the real object or its callers
  • You need to add caching, logging, retry, or rate limiting as a cross-cutting concern
  • You're representing an object that lives in another process or machine (Remote Proxy)
  • You want to implement a circuit breaker, circuit limiter, or health monitor around a service

⚠️ When NOT to Use Proxy:

  • The subject interface is very large — implementing it in every proxy becomes a maintenance burden (use delegation, dynamic proxies, or AOP instead)
  • The behavior you're adding is tightly coupled to the business logic — inline it rather than creating a separate proxy
  • You need to extend or modify behavior rather than control access — use Decorator instead
  • The overhead of indirection outweighs the benefit for trivially cheap operations

🛠️ Best Practices Across Languages:

  1. Always implement the full interface: Every method must be delegated; partial proxies cause NotImplementedException or AttributeError surprises
  2. Keep proxies single-responsibility: One proxy = one concern (caching, logging, or auth — not all three)
  3. Chain proxies for multiple concerns: Compose proxy chains in a factory function to keep client code clean
  4. Make the factory the single assembly point: Build the entire proxy chain in one place so configuration is centralized
  5. Thread-safe lazy initialization: Always use double-checked locking or language idioms (lazy in Kotlin, once.Do in Go) for thread safety
  6. Don't cache null or error results: Cache only successful responses to avoid poisoning the cache
  7. Propagate errors faithfully: A proxy must not swallow exceptions from the real subject
  8. Test each proxy layer independently: Mock the inner subject to verify proxy logic in isolation

💡 Common Pitfalls:

  • Incomplete interface implementation: A proxy that doesn't implement all methods breaks Liskov Substitution Principle
  • Proxy doing too much: A proxy that adds validation, transformation, and caching is a service, not a proxy
  • Thread-unsafe lazy init: Two threads creating the real subject simultaneously wastes resources or causes bugs
  • Caching failures: Caching null/error responses means valid future requests always fail until cache expires
  • Coupling to concrete types: Type-hinting concrete classes in client code prevents proxies from being injected
  • Breaking the chain: Using instanceof checks or downcasting in client code to get the "real" implementation defeats the purpose

🎁 Advanced Techniques:

  • Dynamic Proxy: Use language-native dynamic proxies (Java Proxy, Python __getattr__, JS Proxy, C# DispatchProxy) for automatic interception without per-method boilerplate
  • AOP (Aspect-Oriented Programming): Frameworks like Spring AOP and PostSharp generate proxies from annotations/attributes, eliminating manual proxy classes entirely
  • Proxy + Decorator combo: Use Proxy to control access and Decorator to add behavior — stack them in the right order
  • Circuit Breaker Proxy: Automatically open/close based on downstream failure rate — the production-grade resilience pattern
  • Proxy chains with middleware: HTTP middleware pipelines (Django, Express, ASP.NET) are a chain of request proxies — the same pattern at the framework level

The Proxy Pattern is one of the most widely used patterns in production systems, often invisibly. Every time you use an ORM, call a gRPC service, use Spring's @Cacheable, or write Express middleware — you're already using Proxy. Understanding the pattern explicitly lets you apply it deliberately: to keep your domain logic clean, compose cross-cutting behaviors without pollution, and build systems that can evolve one concern at a time.


Want to dive deeper into design patterns? Check out our comprehensive guides on: