Proxy Pattern: Control Object Access in OOP

Updated 2026-03-11

TL;DR

The Proxy Pattern provides a placeholder or surrogate object that controls access to another object. It acts as an intermediary, allowing you to add functionality like lazy initialization, access control, logging, or caching without modifying the original object.

Prerequisites: Understanding of interfaces and abstract classes, basic inheritance concepts, familiarity with composition over inheritance principle, and knowledge of polymorphism.

After this topic: Implement proxy objects that control access to real objects, identify scenarios where proxies add value (lazy loading, security, caching), and design proxy hierarchies that maintain the same interface as the real subject.

Core Concept

What is the Proxy Pattern?

The Proxy Pattern is a structural design pattern that provides a substitute or placeholder for another object. The proxy controls access to the original object, allowing you to perform operations before or after the request reaches the real object.

Think of a proxy like a representative or agent. When you hire a lawyer, they act as your proxy in legal matters — they represent you, but they can also add value by filtering requests, providing advice, or handling paperwork before matters reach you.

Why Use a Proxy?

Proxies solve several common problems:

  1. Lazy Initialization (Virtual Proxy): Delay creating expensive objects until they’re actually needed
  2. Access Control (Protection Proxy): Add authentication or authorization checks before allowing operations
  3. Remote Access (Remote Proxy): Represent objects in different address spaces (different machines, processes)
  4. Logging/Monitoring (Logging Proxy): Track operations performed on the real object
  5. Caching (Caching Proxy): Store results of expensive operations for reuse

Structure

The pattern involves three key components:

  • Subject Interface: Defines the common interface for both RealSubject and Proxy
  • RealSubject: The actual object that does the real work
  • Proxy: Maintains a reference to RealSubject and controls access to it

The proxy implements the same interface as the real subject, making them interchangeable from the client’s perspective. This is crucial — clients shouldn’t know whether they’re working with a proxy or the real object.

When to Use

Consider the Proxy Pattern when:

  • You need to control access to an object
  • You want to add functionality without modifying the original class
  • Object creation is expensive and should be deferred
  • You need to track or log object usage
  • You’re working with remote objects or resources

Visual Guide

Proxy Pattern Structure

classDiagram
    class Subject {
        <<interface>>
        +request()
    }
    class RealSubject {
        +request()
    }
    class Proxy {
        -realSubject: RealSubject
        +request()
    }
    class Client
    
    Subject <|.. RealSubject
    Subject <|.. Proxy
    Proxy o-- RealSubject
    Client --> Subject

The Proxy and RealSubject both implement the Subject interface. The Proxy holds a reference to RealSubject and delegates requests to it, potentially adding extra behavior.

Proxy Request Flow

sequenceDiagram
    participant Client
    participant Proxy
    participant RealSubject
    
    Client->>Proxy: request()
    Note over Proxy: Pre-processing<br/>(auth, logging, etc.)
    Proxy->>RealSubject: request()
    RealSubject-->>Proxy: result
    Note over Proxy: Post-processing<br/>(caching, cleanup)
    Proxy-->>Client: result

The proxy intercepts client requests, performs additional operations, delegates to the real subject, and can process the response before returning it.

Examples

Example 1: Virtual Proxy (Lazy Loading)

A virtual proxy delays the creation of an expensive object until it’s actually needed. This is common with large images, database connections, or heavy computations.

from abc import ABC, abstractmethod
import time

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

class RealImage(Image):
    """RealSubject - expensive to create"""
    def __init__(self, filename):
        self.filename = filename
        self._load_from_disk()
    
    def _load_from_disk(self):
        print(f"Loading image from disk: {self.filename}")
        time.sleep(2)  # Simulate expensive loading
    
    def display(self):
        print(f"Displaying {self.filename}")

class ImageProxy(Image):
    """Proxy - delays creation until needed"""
    def __init__(self, filename):
        self.filename = filename
        self._real_image = None  # Not created yet!
    
    def display(self):
        # Lazy initialization: create only when needed
        if self._real_image is None:
            self._real_image = RealImage(self.filename)
        self._real_image.display()

# Client code
print("Creating proxy...")
image = ImageProxy("large_photo.jpg")
print("Proxy created (image not loaded yet)\n")

print("First display call:")
image.display()
print()

print("Second display call:")
image.display()

Expected Output:

Creating proxy...
Proxy created (image not loaded yet)

First display call:
Loading image from disk: large_photo.jpg
Displaying large_photo.jpg

Second display call:
Displaying large_photo.jpg

Key Points:

  • The proxy is created instantly without loading the image
  • The real image loads only on first display() call
  • Subsequent calls use the already-loaded image
  • Client code doesn’t know it’s using a proxy

Try it yourself: Add a get_size() method that also triggers lazy loading. How would you handle multiple methods that need the real object?


Example 2: Protection Proxy (Access Control)

A protection proxy controls access based on permissions or authentication.

from abc import ABC, abstractmethod
from typing import Optional

class BankAccount(ABC):
    """Subject interface"""
    @abstractmethod
    def withdraw(self, amount: float) -> bool:
        pass
    
    @abstractmethod
    def get_balance(self) -> float:
        pass

class RealBankAccount(BankAccount):
    """RealSubject - actual bank account"""
    def __init__(self, balance: float):
        self._balance = balance
    
    def withdraw(self, amount: float) -> bool:
        if amount <= self._balance:
            self._balance -= amount
            print(f"Withdrew ${amount}. New balance: ${self._balance}")
            return True
        print(f"Insufficient funds. Balance: ${self._balance}")
        return False
    
    def get_balance(self) -> float:
        return self._balance

class BankAccountProxy(BankAccount):
    """Protection Proxy - adds authentication"""
    def __init__(self, real_account: RealBankAccount, password: str):
        self._real_account = real_account
        self._password = password
        self._authenticated = False
    
    def authenticate(self, password: str) -> bool:
        if password == self._password:
            self._authenticated = True
            print("Authentication successful")
            return True
        print("Authentication failed")
        return False
    
    def withdraw(self, amount: float) -> bool:
        if not self._authenticated:
            print("Access denied: Please authenticate first")
            return False
        print(f"Authorization check passed for withdrawal of ${amount}")
        return self._real_account.withdraw(amount)
    
    def get_balance(self) -> float:
        if not self._authenticated:
            print("Access denied: Please authenticate first")
            return 0.0
        return self._real_account.get_balance()

# Client code
real_account = RealBankAccount(1000.0)
proxy = BankAccountProxy(real_account, "secret123")

print("Attempting withdrawal without authentication:")
proxy.withdraw(100)
print()

print("Authenticating with wrong password:")
proxy.authenticate("wrong")
print()

print("Authenticating with correct password:")
proxy.authenticate("secret123")
print()

print("Attempting withdrawal after authentication:")
proxy.withdraw(100)
print()

print("Checking balance:")
balance = proxy.get_balance()
print(f"Current balance: ${balance}")

Expected Output:

Attempting withdrawal without authentication:
Access denied: Please authenticate first

Authenticating with wrong password:
Authentication failed

Authenticating with correct password:
Authentication successful

Attempting withdrawal after authentication:
Authorization check passed for withdrawal of $100
Withdrew $100. New balance: $900.0

Checking balance:
Current balance: $900.0

Key Points:

  • Proxy enforces authentication before delegating to real account
  • All methods check authentication state
  • Real account logic remains unchanged
  • Security concerns are separated from business logic

Try it yourself: Add role-based access control where only “admin” users can withdraw more than $500.


Example 3: Caching Proxy

A caching proxy stores results of expensive operations to avoid redundant work.

from abc import ABC, abstractmethod
import time
from typing import Dict

class DataService(ABC):
    """Subject interface"""
    @abstractmethod
    def fetch_user_data(self, user_id: int) -> dict:
        pass

class RealDataService(DataService):
    """RealSubject - makes expensive API calls"""
    def fetch_user_data(self, user_id: int) -> dict:
        print(f"Fetching data for user {user_id} from database...")
        time.sleep(1)  # Simulate network delay
        return {
            "id": user_id,
            "name": f"User{user_id}",
            "email": f"user{user_id}@example.com"
        }

class CachingDataServiceProxy(DataService):
    """Caching Proxy - stores results"""
    def __init__(self, real_service: DataService):
        self._real_service = real_service
        self._cache: Dict[int, dict] = {}
    
    def fetch_user_data(self, user_id: int) -> dict:
        if user_id in self._cache:
            print(f"Returning cached data for user {user_id}")
            return self._cache[user_id]
        
        print(f"Cache miss for user {user_id}")
        data = self._real_service.fetch_user_data(user_id)
        self._cache[user_id] = data
        return data
    
    def clear_cache(self):
        """Additional proxy-specific method"""
        self._cache.clear()
        print("Cache cleared")

# Client code
real_service = RealDataService()
proxy = CachingDataServiceProxy(real_service)

print("First request for user 1:")
data1 = proxy.fetch_user_data(1)
print(f"Data: {data1}\n")

print("Second request for user 1 (should be cached):")
data1_again = proxy.fetch_user_data(1)
print(f"Data: {data1_again}\n")

print("Request for user 2:")
data2 = proxy.fetch_user_data(2)
print(f"Data: {data2}\n")

print("Request for user 1 again:")
data1_third = proxy.fetch_user_data(1)
print(f"Data: {data1_third}")

Expected Output:

First request for user 1:
Cache miss for user 1
Fetching data for user 1 from database...
Data: {'id': 1, 'name': 'User1', 'email': 'user1@example.com'}

Second request for user 1 (should be cached):
Returning cached data for user 1
Data: {'id': 1, 'name': 'User1', 'email': 'user1@example.com'}

Request for user 2:
Cache miss for user 2
Fetching data for user 2 from database...
Data: {'id': 2, 'name': 'User2', 'email': 'user2@example.com'}

Request for user 1 again:
Returning cached data for user 1
Data: {'id': 1, 'name': 'User1', 'email': 'user1@example.com'}

Key Points:

  • First request is slow (1 second delay)
  • Subsequent requests for same user are instant
  • Cache is transparent to the client
  • Proxy can have additional methods like clear_cache()

Java/C++ Notes:

  • In Java, use HashMap<Integer, UserData> for the cache
  • In C++, use std::unordered_map<int, UserData> and smart pointers (std::shared_ptr) for the real service reference
  • Both languages benefit from explicit interface definitions

Try it yourself: Add a time-to-live (TTL) feature where cached data expires after 5 seconds.

Common Mistakes

1. Breaking the Interface Contract

Mistake: The proxy doesn’t implement the same interface as the real subject, or adds required parameters that the real subject doesn’t have.

# WRONG - Proxy has different interface
class ImageProxy:
    def display(self, force_reload=False):  # Extra parameter!
        pass

class RealImage:
    def display(self):  # Different signature
        pass

Why it’s wrong: Clients can’t use proxy and real subject interchangeably. This defeats the purpose of the pattern.

Fix: Both must implement the same interface exactly. Add proxy-specific methods separately if needed.

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

class ImageProxy(Image):
    def display(self):
        # Implementation
        pass
    
    def force_reload(self):  # Separate method for proxy-specific behavior
        pass

2. Forgetting to Delegate

Mistake: The proxy performs operations itself instead of delegating to the real subject.

# WRONG - Proxy doesn't delegate
class BankAccountProxy:
    def __init__(self, real_account):
        self._real_account = real_account
        self._balance = 1000  # Proxy maintains its own state!
    
    def withdraw(self, amount):
        if self._authenticated:
            self._balance -= amount  # Wrong: not delegating

Why it’s wrong: The proxy duplicates logic and state, leading to inconsistencies. The real subject and proxy can get out of sync.

Fix: Proxy should only add cross-cutting concerns (auth, logging, caching) and delegate actual work.

# CORRECT
class BankAccountProxy:
    def withdraw(self, amount):
        if self._authenticated:
            return self._real_account.withdraw(amount)  # Delegate!

3. Creating the Real Object Too Early

Mistake: In a virtual proxy, creating the real object in the proxy’s constructor defeats lazy initialization.

# WRONG - Defeats lazy loading
class ImageProxy:
    def __init__(self, filename):
        self._real_image = RealImage(filename)  # Created immediately!

Why it’s wrong: The expensive object is created even if never used. You lose the performance benefit.

Fix: Create the real object only when first needed.

# CORRECT
class ImageProxy:
    def __init__(self, filename):
        self.filename = filename
        self._real_image = None  # Not created yet
    
    def display(self):
        if self._real_image is None:
            self._real_image = RealImage(self.filename)  # Lazy creation
        self._real_image.display()

4. Not Handling All Methods

Mistake: Implementing proxy logic for some methods but forgetting others in the interface.

# WRONG - Incomplete proxy
class BankAccountProxy(BankAccount):
    def withdraw(self, amount):
        if self._authenticated:
            return self._real_account.withdraw(amount)
    
    def get_balance(self):
        # Forgot to check authentication!
        return self._real_account.get_balance()

Why it’s wrong: Security holes or inconsistent behavior. Some operations bypass proxy logic.

Fix: Apply proxy logic consistently to all interface methods.


5. Exposing the Real Subject

Mistake: Providing direct access to the real subject, allowing clients to bypass the proxy.

# WRONG - Exposes real subject
class ImageProxy:
    def get_real_image(self):
        return self._real_image  # Clients can bypass proxy!

Why it’s wrong: Clients can circumvent access control, caching, or other proxy functionality.

Fix: Keep the real subject private. Never expose it through public methods.

Interview Tips

What Interviewers Look For

1. Recognize When to Use Proxies

Interviewers often present scenarios and ask you to choose a pattern. Be ready to identify proxy use cases:

  • “How would you implement lazy loading for large images?”
  • “Design a system to control access to sensitive operations”
  • “How would you add caching without modifying existing code?”

Strong answer: Identify the specific proxy type (virtual, protection, caching) and explain why it fits. Mention that the proxy maintains the same interface as the real subject.


2. Explain the Difference from Similar Patterns

You’ll likely be asked: “How is Proxy different from Decorator or Adapter?”

Key distinctions:

  • Proxy vs Decorator: Proxy controls access and manages the lifecycle of the real object. Decorator adds new functionality. Proxy often creates/destroys the real subject; decorator wraps an existing object.
  • Proxy vs Adapter: Proxy has the same interface as the real subject. Adapter converts one interface to another.
  • Proxy vs Facade: Proxy represents a single object with the same interface. Facade provides a simplified interface to a complex subsystem.

Interview tip: Draw a quick diagram showing the relationships. Mention that proxy and decorator have similar structures but different intents.


3. Discuss Trade-offs

Be prepared to discuss disadvantages:

  • Adds an extra layer of indirection (slight performance overhead)
  • Increases code complexity
  • Can make debugging harder (which object is actually being called?)

Strong answer: “While proxies add a layer of indirection, the benefits usually outweigh the costs. For example, lazy loading can save significant memory, and the performance cost of one extra method call is negligible compared to loading a 10MB image.”


4. Code It Correctly

If asked to implement a proxy:

  1. Start with the interface/abstract class
  2. Implement the real subject
  3. Implement the proxy with the same interface
  4. Show the proxy delegating to the real subject
  5. Add the specific proxy behavior (caching, auth, etc.)

Common interview question: “Implement a caching proxy for a database query service.”

Approach:

  • Define the service interface
  • Create a real service that “queries the database” (simulate with sleep)
  • Create a proxy that checks a cache dictionary before delegating
  • Demonstrate with example usage

5. Real-World Examples

Mention concrete examples to show practical knowledge:

  • Virtual proxies: ORM frameworks (Hibernate, SQLAlchemy) use lazy loading for related objects
  • Protection proxies: Spring Security uses proxies to enforce method-level security
  • Remote proxies: RPC frameworks, REST clients, database connection pools
  • Smart references: Python’s weakref proxy, C++ smart pointers

Interview tip: If you’ve used any of these technologies, mention it: “In my last project, we used SQLAlchemy’s lazy loading, which is essentially a virtual proxy pattern.”


6. Handle Follow-up Questions

Be ready for deeper questions:

  • “How would you implement a thread-safe caching proxy?” (Answer: Use locks or thread-safe collections)
  • “What if the real object creation fails?” (Answer: Proxy should handle exceptions and possibly retry)
  • “How do you test proxies?” (Answer: Mock the real subject, verify proxy behavior separately)

Red Flags to Avoid:

  • Saying proxy and decorator are “basically the same”
  • Implementing a proxy that doesn’t match the real subject’s interface
  • Not being able to name a specific proxy type (virtual, protection, remote, etc.)
  • Forgetting to delegate to the real subject

Key Takeaways

  • The Proxy Pattern provides a surrogate that controls access to another object while maintaining the same interface, enabling lazy initialization, access control, caching, or logging without modifying the real subject.

  • Proxy and real subject must implement the same interface to be interchangeable from the client’s perspective. The proxy delegates actual work to the real subject after performing its own operations.

  • Four main proxy types solve different problems: Virtual proxies delay expensive object creation, protection proxies enforce access control, remote proxies represent objects in different address spaces, and caching proxies store results to avoid redundant operations.

  • Proxies add a layer of indirection which introduces slight overhead but provides significant benefits like improved performance (lazy loading, caching), enhanced security (access control), and better separation of concerns.

  • Distinguish proxies from similar patterns: Unlike decorators (which add functionality to existing objects), proxies control access and manage object lifecycle. Unlike adapters (which change interfaces), proxies maintain the same interface as the real subject.