Provide a substitute or placeholder for another object to control access to it — adding lazy loading, caching, logging, or access control transparently.
The Proxy pattern provides a stand-in object that controls access to another object (the real subject). The proxy implements the same interface as the real subject, so the client can't tell the difference — but the proxy intercepts every call and can add behaviour before or after forwarding.
Think of a bank card: the card isn't your bank account — it's a proxy. It controls access (PIN check), logs transactions, enforces limits, and only then lets the operation through to the real account.
The key distinction from Decorator: the Proxy controls access to the subject, while Decorator adds behaviour. Proxy often manages the subject's lifecycle (creating it lazily). Decorator receives the subject from outside.
Key Idea: Same interface as the real object, but with a gatekeeper in front. Intercept calls to add caching, logging, access control, or lazy initialisation — transparently.
Creates the real object only when first needed. Useful when the real object is expensive to create (large file, DB connection). The proxy stands in until the real object is actually required.
Controls who can call which operations on the real object. Checks permissions before forwarding calls. The real object has no auth logic — the proxy is the gatekeeper.
Caches results of expensive operations. Returns cached data for repeated identical requests, only calling the real object on a cache miss or after TTL expires.
Records every call made to the real object — method name, arguments, timing, return value. The real object stays clean of logging concerns.
Represents an object that lives in a different process or machine. Handles serialisation, network communication, and error handling — the client thinks it's calling a local object.
| Aspect | Proxy | Decorator |
|---|---|---|
| Purpose | Control access to the subject | Add behaviour to the subject |
| Subject lifecycle | Proxy may create/manage it | Decorator receives it from outside |
| Stacking | Usually one proxy layer | Often stacked multiple times |
| Client intent | Access control / transparency | Feature extension |
// Proxy Pattern — Virtual + Protection + Caching Proxy
// ─── Subject interface ────────────────────────────────────────
interface ImageService {
String loadImage(String filename);
void deleteImage(String filename);
}
// ─── Real Subject ─────────────────────────────────────────────
class RealImageService implements ImageService {
public String loadImage(String filename) {
System.out.println("[RealService] Loading from disk: " + filename);
// Simulate expensive disk I/O
return "ImageData:" + filename;
}
public void deleteImage(String filename) {
System.out.println("[RealService] Deleted: " + filename);
}
}
// ─── Virtual + Caching Proxy ──────────────────────────────────
class CachingImageProxy implements ImageService {
private RealImageService realService;
private java.util.Map<String, String> cache = new java.util.HashMap<>();
private RealImageService getService() {
if (realService == null) {
System.out.println("[Proxy] Lazily initialising RealImageService...");
realService = new RealImageService(); // created only on first use
}
return realService;
}
@Override
public String loadImage(String filename) {
if (cache.containsKey(filename)) {
System.out.println("[Proxy] Cache HIT: " + filename);
return cache.get(filename);
}
System.out.println("[Proxy] Cache MISS: " + filename);
String data = getService().loadImage(filename);
cache.put(filename, data);
return data;
}
@Override
public void deleteImage(String filename) {
cache.remove(filename); // invalidate cache
getService().deleteImage(filename);
}
}
// ─── Protection Proxy ─────────────────────────────────────────
class ProtectedImageProxy implements ImageService {
private ImageService inner;
private String userRole;
public ProtectedImageProxy(ImageService inner, String userRole) {
this.inner = inner;
this.userRole = userRole;
}
@Override
public String loadImage(String filename) {
return inner.loadImage(filename); // all users can read
}
@Override
public void deleteImage(String filename) {
if (!userRole.equals("ADMIN")) {
throw new SecurityException("Only admins can delete images!");
}
inner.deleteImage(filename);
}
}
// ─── Client ───────────────────────────────────────────────────
public class App {
public static void main(String[] args) {
ImageService cache = new CachingImageProxy();
System.out.println(cache.loadImage("photo.jpg")); // miss → loads
System.out.println(cache.loadImage("photo.jpg")); // hit → cached
System.out.println(cache.loadImage("logo.png")); // miss → loads
// Wrap cache in a protection proxy
ImageService adminView = new ProtectedImageProxy(cache, "ADMIN");
ImageService userView = new ProtectedImageProxy(cache, "USER");
adminView.deleteImage("photo.jpg"); // allowed
try {
userView.deleteImage("logo.png"); // throws
} catch (SecurityException e) {
System.out.println("[Access Denied] " + e.getMessage());
}
}
}When a resource-heavy object should only be created on first use — database connections, large file loads, expensive calculations.
VirtualImageProxy, LazyConnectionProxyWhen only certain users or roles should be able to call specific methods — the proxy checks permissions before forwarding.
ProtectedFileProxy, RoleBasedServiceProxyWhen results of expensive service calls should be cached without changing the service interface — the client code stays unchanged.
CachedAPIProxy, MemoizedQueryProxyWhen representing a remote service locally — the proxy handles serialisation, network calls, and error handling transparently.
gRPC stubs, REST client proxies, RMIWhen you need to log all method calls, track performance, or capture metrics without cluttering business logic.
LoggingServiceProxy, MetricsProxySpring's Aspect-Oriented Programming wraps beans in dynamic proxies. @Transactional, @Cacheable, @Secured annotations all work by creating a proxy around the bean that intercepts calls.
Mock objects are Proxy implementations — they stand in for real objects during tests, recording all calls made to them and returning configured values.
JavaScript has Proxy as a first-class language feature. Vue 3's reactivity system uses Proxy to intercept property reads/writes and trigger DOM updates automatically.
Hibernate uses virtual proxies for lazy-loaded relationships. An @OneToMany collection is a proxy object — the DB query only fires when you actually access the collection's contents.