On this page
Description
Proxy is the structural pattern for standing in front of an object and controlling access to it: the proxy has the same interface, so callers can't tell the difference, but it decides when the real object is created, whether a call is allowed, whether the call is needed at all, or where it really runs. Lazy loading in ORMs, caching clients, security checks and remote stubs are all proxies. This skill covers the kinds of proxy, how they work, and where they surprise people.
A photo gallery shows 500 thumbnails, but each HighResImage loads a 20 MB file in its constructor. Creating the list takes forever, even though the user will open maybe three photos.
A second, related problem: you want every call to a document store to be permission-checked and logged. Putting those checks inside every method of the store mixes security code with storage code - and it's easy to forget one.
We'll go from simple to more technical:
- The pattern: same interface, controlled access.
- The kinds of proxy - virtual, protection, caching, remote, logging.
- Where you'll see proxies: ORMs, Spring, RPC stubs, CDNs.
- The surprises: self-invocation, lazy loading after the session closes, stale caches and chatty remote calls.
The pattern
A Proxy implements the same interface as the real object and stands in for it, controlling access: when the real object is created, whether a call is allowed, or whether it's needed at all.
Java
interface Image {
void display();
}
class HighResImage implements Image {
HighResImage(String path) { /* expensive: loads 20 MB from disk */ }
public void display() { /* draw it */ }
}
class LazyImageProxy implements Image {
private final String path;
private HighResImage real; // not loaded yet
LazyImageProxy(String path) { this.path = path; } // cheap
public void display() {
if (real == null) real = new HighResImage(path); // load on first real use
real.display();
}
}
Python
from abc import ABC, abstractmethod
class Image(ABC):
@abstractmethod
def display(self) -> None: ...
class HighResImage(Image):
def __init__(self, path: str):
... # expensive: loads 20 MB from disk
def display(self) -> None:
... # draw it
class LazyImageProxy(Image):
def __init__(self, path: str): # cheap
self._path = path
self._real: HighResImage | None = None # not loaded yet
def display(self) -> None:
if self._real is None:
self._real = HighResImage(self._path) # load on first real use
self._real.display()
The gallery holds 500 cheap proxies; only the photos the user actually opens are ever loaded.
How it works, step by step
- Proxy and real object implement the same interface, so clients can't tell them apart.
- The proxy holds (or knows how to get) the real object.
- Before forwarding a call it applies its control: load lazily, check permissions, return a cached value, or send the call over the network.
- Common kinds: virtual (lazy loading), protection (access checks), caching, remote (stand-in for an object on another machine) and logging proxies.
Where you'll see it
- Hibernate/JPA lazy associations:
order.getCustomer()returns a proxy that loads the customer on first use. - Spring wraps your beans in proxies to add
@Transactional,@Cacheableand security behavior. java.lang.reflect.Proxycreates proxies at runtime; gRPC and RMI client stubs are remote proxies.- In Python, SQLAlchemy's lazy relationships load on first access,
functools.cached_propertycomputes an attribute on first use, and a class with__getattr__can forward every call to a real object it creates - or connects to - only when needed. - In infrastructure, reverse proxies and CDNs cache and protect servers the same way.
Choosing among the structural patterns
| If you need to... | Reach for | What it changes |
|---|---|---|
| Make an existing class fit the interface your code expects | Adapter | Changes the interface; the behavior stays the same. |
| Add behavior to an object without changing its class or interface | Decorator | Same interface, extra behavior, and wrappers can be stacked. |
| Give a complicated subsystem one simple front door | Facade | A new, simpler interface over many classes. |
| Treat a single item and a group of items the same way | Composite | A tree: groups contain items or other groups. |
| Control access to an object (lazy loading, caching, security, remote calls) | Proxy (this lesson) | Same interface, but it decides when and whether the real object is used. |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
Calling a proxied method on this / self | the proxy's behavior (transactions, caching, checks) is skipped | call through the proxy, or move the method to another object |
| Touching a lazy ORM proxy after the session closes | LazyInitializationException or a detached-instance error | load what you need inside the session |
| Lazy proxies in a loop over a list | one query per item (N+1) | fetch the related data in one query |
| A caching proxy with no invalidation plan | stale data served indefinitely | a TTL, event-based invalidation, or both |
| Treating a remote proxy like a local object | hundreds of round trips, unhandled timeouts | coarse-grained calls and explicit failure handling |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. Name four kinds of proxy and what each one controls.
Show answer
Virtual - when the real object is created. Protection - whether a call is allowed. Caching - whether the real call is needed at all. Remote - where the object actually runs.
2. A Spring bean calls its own @Transactional method through this. Does a transaction start?
Show answer
No. The call goes straight to the real object and never passes through the proxy that starts transactions. Move the method to another bean, or call it through the proxy.
3. Proxy and Decorator both share the interface and wrap an object. What is the difference?
Show answer
Intent. A proxy controls access to one subject and is usually invisible to the client, often managing the subject's lifecycle itself. A decorator adds behavior and is composed deliberately by the client.
Key takeaways
- A proxy has the same interface as the real object and controls access to it.
- Kinds: virtual (lazy), protection (security), caching, remote and logging.
- Calls that don't go through the proxy (self-invocation) skip its behavior.
- Caching proxies need an invalidation strategy.
- Decorator adds behavior; Proxy controls access.