One-line Definition
Encapsulates access to an object via a surrogate, allowing you to intercept and control operations dynamically.
Problem
Suppose you have a heavy object like a massive video file or a remote database connection. It consumes vast resources to instantiate or query. If you load it immediately upon startup, you waste resources, but if you scatter "lazy initialization" or caching logic all over your client code, you violate the Single Responsibility Principle. A Proxy solves this by standing in for the real object, looking exactly like it, but handling the caching, security, or lazy-loading behind the scenes.
Real-world Analogy
A credit card is a proxy for your bank account. When you swipe at a store, the card doesn't hand over cash — it checks your credit limit, authenticates you, and then authorizes the payment on your behalf. The store doesn't interact with your bank directly. The credit card controls access to your money.
Structure
- Client
- Works with objects strictly via the Subject interface, unaware if it holds a Proxy or the RealSubject.
- Subject
- Interface shared by both the real object and the proxy.
- Proxy
- Wraps the RealSubject and adds control logic (access checks, caching, lazy init) before or after delegating to the real object.
- RealSubject
- The actual object that does the heavy lifting.
Diagram
classDiagram
class Client {
}
class Subject {
<<interface>>
+request()
}
class Proxy {
-RealSubject realSubject
+request()
}
class RealSubject {
+request()
}
Client --> Subject
Proxy ..|> Subject
RealSubject ..|> Subject
Proxy --> RealSubject : controls access
Code Walkthrough
Notice how the CachingProxy intercepts the execute() call. It checks its cache first, and only delegates to RealDatabase if the result is missing, keeping the client completely unaware of the optimization.
interface DatabaseQuery {
String execute(String query);
}
class RealDatabase implements DatabaseQuery {
@Override
public String execute(String query) {
System.out.println("Executing heavy query: " + query);
// Simulate expensive DB call
return "Result for: " + query;
}
}
class CachingProxy implements DatabaseQuery {
private final RealDatabase realDatabase;
private final Map<String, String> cache = new HashMap<>();
public CachingProxy(RealDatabase realDatabase) {
this.realDatabase = realDatabase;
}
@Override
public String execute(String query) {
if (cache.containsKey(query)) {
System.out.println("Cache hit for: " + query);
return cache.get(query);
}
String result = realDatabase.execute(query);
cache.put(query, result);
return result;
}
}
class Main {
public static void main(String[] args) {
RealDatabase realDb = new RealDatabase();
DatabaseQuery db = new CachingProxy(realDb);
System.out.println(db.execute("SELECT * FROM users")); // DB call
System.out.println(db.execute("SELECT * FROM users")); // Cache hit
System.out.println(db.execute("SELECT * FROM orders")); // DB call
}
}
Bad vs Good
Bad Approach
Problems
- The client handles the caching logic directly, violating the Single Responsibility Principle.
- If multiple clients query the database, the caching logic is duplicated everywhere.
class ClientService {
private final RealDatabase db = new RealDatabase();
private final Map<String, String> cache = new HashMap<>();
public String getUserData(String query) {
if (cache.containsKey(query)) {
return cache.get(query);
}
String result = db.execute(query);
cache.put(query, result);
return result;
}
}
Better Approach
Improvements
- Client relies entirely on the interface.
- Caching, security, or logging can be added via the Proxy, keeping the client code clean and focused on business logic.
// Client simply requests data
DatabaseQuery db = new CachingProxy(new RealDatabase());
String data = db.execute("SELECT * FROM users"); // Caching handled invisibly
Pros vs Cons
| Pros | Cons |
|---|---|
| Controls access to an object without modifying client code | Adds an extra layer of indirection |
| Centralizes cross-cutting concerns (security, caching, logging) | Can introduce latency in performance-critical paths |
| Follows Open/Closed Principle — proxy acts as a non-invasive wrapper | Proxy and real object must strictly share the same interface |
| Manages the lifecycle of heavy objects (lazy loading) | Can become difficult to trace logic flow (too much "magic") |
| Fully transparent to the client |
When to Use
- Lazy initialization (Virtual Proxy): You have a heavy object you want to delay creating until it's actually needed.
- Access control (Protection Proxy): You want specific clients to have different permissions when calling a method.
- Local execution of remote service (Remote Proxy): You want to hide network communication logic from the client.
- Caching (Caching Proxy): You want to cache results of expensive operations.
When Not to Use
- Direct access to the object is inexpensive and requires no special rules or caching.
- Adding a proxy would just add an unnecessary layer of wrappers with no distinct behavioral changes.
Real-world Examples
java.lang.reflect.Proxy(Java's built-in dynamic proxy mechanism)- Spring AOP (Aspect-Oriented Programming uses proxies for
@Transactional,@Cacheable, etc.) - Hibernate's lazy-loaded entity collections.
Key Takeaway
The Proxy Pattern intercepts access to an object through a surrogate that shares the exact same interface, letting you add security, caching, lazy loading, or networking logic completely invisibly. It is the backbone of modern framework "magic" (like Spring AOP), but avoid creating proxies if direct access works fine.