Chapter 04 · Article 18 of 55
Proxy Pattern - Deep Dive
Intent: Provide a surrogate or placeholder for another object to control access to it.
Article outline15 sections on this page+
Overview
Intent: Provide a surrogate or placeholder for another object to control access to it.
The Proxy pattern introduces an intermediary object that stands between the client and the real service object. The proxy implements the same interface as the real object, making it interchangeable from the client's perspective. This indirection enables powerful capabilities - lazy initialization, access control, logging, caching, and remote communication - all without the client knowing it is not talking to the real object directly.
The pattern belongs to the Structural family in the Gang of Four classification. It is one of the most frequently encountered patterns in enterprise systems, frameworks (Spring, Hibernate), and distributed architectures.
Also known as: Surrogate
Problem It Solves
Without the Proxy pattern, you face these recurring challenges:
- Expensive Object Creation - A resource-heavy object (large image, database connection, remote service stub) is created eagerly even when it may never be used, wasting memory and startup time.
- Uncontrolled Access - Any client can invoke any operation on a service object with no permission checks, violating the principle of least privilege.
- Remote Transparency - Clients must handle network details (serialization, connection management, retries) when calling remote services, coupling business logic to infrastructure.
- Cross-Cutting Concerns - Adding logging, metrics, or caching to an existing class requires modifying its source, violating the Open/Closed Principle.
- Resource Management - There is no centralized place to track how many clients hold references to a heavy object or when it can be safely released.
The Proxy pattern solves all of these by inserting a lightweight stand-in that controls when, how, and whether the real object is accessed.
When to Use / When NOT to Use
| When to Use | When NOT to Use |
|---|---|
| Object creation is expensive and may not always be needed (virtual proxy) | The object is lightweight and always needed immediately |
| You need access control without modifying the real subject | Simple delegation with no added behavior (just adds indirection) |
| The real object lives on a remote server (remote proxy) | You need to change the interface (use Adapter instead) |
| You want to cache results transparently | Caching logic is already handled at a different layer |
| You need logging/auditing around method calls | The class is final/sealed and cannot be proxied easily |
| You want reference counting or smart pointer semantics | You need to add multiple independent behaviors dynamically (use Decorator) |
| Framework requires proxy (Spring beans, JPA entities) | Performance-critical hot path where proxy overhead is unacceptable |
Key Concepts & Theory
Core Participants
- Subject (Interface): Declares the common interface for RealSubject and Proxy so the proxy can be used anywhere the real subject is expected.
- RealSubject: The actual object that does the real work.
- Proxy: Maintains a reference to the RealSubject, controls access to it, and may be responsible for creating or destroying it.
Types of Proxies
| Type | Purpose | Key Behavior |
|---|---|---|
| Virtual Proxy | Lazy initialization | Defers creation of expensive object until first use |
| Protection Proxy | Access control | Checks caller permissions before delegating |
| Remote Proxy | Network transparency | Represents an object in a different address space |
| Caching Proxy | Performance optimization | Stores results and serves repeated requests from cache |
| Logging Proxy | Auditing & monitoring | Records method invocations, parameters, and timing |
| Smart Reference | Resource management | Counts references, locks resources, or loads on first dereference |
Design Principles Applied
- Single Responsibility: Each proxy type handles exactly one concern.
- Open/Closed: New behavior is added without modifying the real subject.
- Liskov Substitution: Proxy is substitutable for the real subject via the shared interface.
- Dependency Inversion: Clients depend on the Subject abstraction, not the concrete class.
ASCII Class Diagram
┌─────────────────────────┐
│ <<interface>> │
│ Subject │
├─────────────────────────┤
│ + request(): void │
│ + getData(): Data │
└────────────┬────────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────────┐
│ RealSubject │ │ Proxy │
├─────────────────────┤ ├─────────────────────────┤
│ - heavyData: Data │ │ - realSubject: Subject │
├─────────────────────┤ │ - cached: Data │
│ + request(): void │ ├─────────────────────────┤
│ + getData(): Data │ │ + request(): void │
└─────────────────────┘ │ + getData(): Data │
└─────────────────────────┘
│
│ delegates to
▼
┌─────────────────────┐
│ RealSubject │
└─────────────────────┘
Client ──────► Proxy ──────► RealSubject
(same interface)
Pseudocode Implementation
1. Virtual Proxy - LazyImageProxy
interface Image {
display(): void
getSize(): int
}
class HighResolutionImage implements Image {
private pixels: byte[]
constructor(filename: string) {
// Expensive: loads 50MB file from disk
this.pixels = loadFromDisk(filename)
log("Loaded heavy image: " + filename)
}
display(): void {
renderToScreen(this.pixels)
}
getSize(): int {
return this.pixels.length
}
}
class LazyImageProxy implements Image {
private realImage: HighResolutionImage = null
private filename: string
constructor(filename: string) {
this.filename = filename // Lightweight - no disk I/O
}
private ensureLoaded(): void {
if (this.realImage == null) {
this.realImage = new HighResolutionImage(this.filename)
}
}
display(): void {
ensureLoaded() // Load only when actually needed
this.realImage.display()
}
getSize(): int {
ensureLoaded()
return this.realImage.getSize()
}
}
// Client code - no awareness of proxy
gallery: List<Image> = [
new LazyImageProxy("photo1.raw"), // No disk I/O yet
new LazyImageProxy("photo2.raw"),
new LazyImageProxy("photo3.raw")
]
gallery[0].display() // Only now photo1.raw is loaded
2. Protection Proxy - AccessControlProxy
interface Document {
read(): string
write(content: string): void
delete(): void
}
class SensitiveDocument implements Document {
private content: string
read(): string { return this.content }
write(content: string): void { this.content = content }
delete(): void { destroyPermanently() }
}
class AccessControlProxy implements Document {
private realDoc: Document
private currentUser: User
constructor(realDoc: Document, user: User) {
this.realDoc = realDoc
this.currentUser = user
}
read(): string {
if (!currentUser.hasPermission("READ")) {
throw AccessDeniedException("No read access")
}
return this.realDoc.read()
}
write(content: string): void {
if (!currentUser.hasPermission("WRITE")) {
throw AccessDeniedException("No write access")
}
this.realDoc.write(content)
}
delete(): void {
if (!currentUser.hasRole("ADMIN")) {
throw AccessDeniedException("Only admins can delete")
}
log("User " + currentUser.name + " deleted document")
this.realDoc.delete()
}
}
// Usage
doc: Document = new AccessControlProxy(sensitiveDoc, currentUser)
doc.read() // Checks READ permission first
doc.delete() // Throws if not ADMIN
3. Caching Proxy - CachingDatabaseProxy
interface DatabaseService {
query(sql: string): ResultSet
execute(sql: string): int
}
class RealDatabaseService implements DatabaseService {
query(sql: string): ResultSet {
// Expensive: network round-trip to DB
return connection.executeQuery(sql)
}
execute(sql: string): int {
return connection.executeUpdate(sql)
}
}
class CachingDatabaseProxy implements DatabaseService {
private realService: DatabaseService
private cache: Map<string, CacheEntry>
private ttl: Duration = 5.minutes
private lock: ReadWriteLock
constructor(realService: DatabaseService) {
this.realService = realService
this.cache = new HashMap()
this.lock = new ReentrantReadWriteLock()
}
query(sql: string): ResultSet {
lock.readLock().acquire()
entry = cache.get(sql)
lock.readLock().release()
if (entry != null && !entry.isExpired(ttl)) {
log("Cache HIT: " + sql)
return entry.result
}
// Cache miss - fetch from real DB
log("Cache MISS: " + sql)
result = this.realService.query(sql)
lock.writeLock().acquire()
cache.put(sql, new CacheEntry(result, now()))
lock.writeLock().release()
return result
}
execute(sql: string): int {
// Writes invalidate cache
lock.writeLock().acquire()
cache.clear()
lock.writeLock().release()
return this.realService.execute(sql)
}
}
Types of Proxy
| Type | Intent | Creates Real Object? | Example | Overhead |
|---|---|---|---|---|
| Virtual | Defer expensive creation | Yes, on first use | Hibernate lazy-loaded entity | Null check per call |
| Protection | Restrict access by role/permission | No (wraps existing) | Spring Security method interceptor | Permission lookup |
| Remote | Hide network communication | No (represents remote) | Java RMI stub, gRPC client stub | Serialization + network |
| Caching | Avoid redundant computation | No (wraps existing) | CDN, ORM query cache | Memory for cache storage |
| Logging | Record invocations transparently | No (wraps existing) | Spring AOP @Around advice | I/O for log writes |
| Smart Reference | Manage lifecycle/ref counting | Yes or wraps existing | C++ shared_ptr, WeakReference | Counter maintenance |
When Each Type Shines
- Virtual: Document editors loading thumbnails instead of full images until zoom.
- Protection: Multi-tenant SaaS where tenant A must never access tenant B's data.
- Remote: Microservice communication where the proxy hides HTTP/gRPC details.
- Caching: Read-heavy APIs where the same query is repeated thousands of times per second.
- Logging: Debugging production issues without redeploying instrumented code.
- Smart Reference: Preventing memory leaks in object pools or connection pools.
Real-World Examples
1. Hibernate Lazy Loading (Virtual Proxy)
Hibernate returns a proxy subclass for @ManyToOne(fetch = LAZY) associations. The proxy holds only the entity ID and intercepts all getter calls. When any property accessor is invoked for the first time, the proxy fires a SQL SELECT to load the real entity from the database. This avoids the N+1 eager loading problem for large object graphs. Internally, Hibernate uses ByteBuddy (previously CGLIB) to generate these proxy subclasses at runtime. A common pitfall is accessing a lazy proxy outside an active Hibernate session, which throws LazyInitializationException.
2. Spring AOP Proxies (Logging/Transaction Proxy)
Spring wraps beans with JDK dynamic proxies (interface-based) or CGLIB proxies (class-based) to apply @Transactional, @Cacheable, @Secured, and custom @Around advice. The client injects the proxy, never the raw bean. When you call a @Transactional method, the proxy opens a transaction before delegating and commits/rolls back after. This is why self-invocation (calling a transactional method from within the same class) bypasses the proxy - the call does not go through the proxy layer. Understanding this limitation is critical for Spring developers.
3. Java RMI (Remote Proxy)
The RMI compiler generates a stub (client-side proxy) and a skeleton (server-side dispatcher). The stub marshals method arguments into a byte stream, sends them over TCP to the skeleton, which unmarshals and invokes the real method on the server. The return value travels back the same way. The entire network layer is transparent to the caller - they invoke methods as if the object were local. Modern equivalents include gRPC client stubs and Apache Thrift clients.
4. CDN as Caching Proxy
A Content Delivery Network sits between users and the origin server. It caches static assets (images, CSS, JS) at geographically distributed edge locations. Clients hit the CDN URL (proxy); the CDN either serves from its local cache (cache hit) or fetches from the origin server, stores the response, then serves it (cache miss). Cache invalidation is controlled via TTL headers or explicit purge APIs. CloudFront, Cloudflare, and Akamai are all implementations of this caching proxy pattern at massive scale.
5. VPN as Protection Proxy
A corporate VPN acts as a protection proxy - it authenticates the user via certificates or MFA, checks device compliance (OS version, antivirus status), and only then tunnels traffic to internal services. Unauthorized users never reach the real network. The VPN endpoint is the proxy; the internal services are the real subjects. Zero-trust architectures extend this concept by applying protection proxy logic at every service boundary, not just the network perimeter.
6. Kubernetes Service (Remote + Load-Balancing Proxy)
A Kubernetes Service object is a virtual proxy that routes traffic to healthy pods. Clients address the service DNS name; kube-proxy handles the actual routing, health checks, and failover transparently. If a pod dies, traffic is automatically redirected to surviving replicas. Service meshes like Istio add sidecar proxies (Envoy) that layer on mTLS, retries, circuit breaking, and observability - combining remote, protection, and logging proxy behaviors in a single infrastructure component.
Proxy vs Decorator vs Adapter
All three patterns wrap an object, but their intent differs fundamentally:
| Aspect | Proxy | Decorator | Adapter |
|---|---|---|---|
| Intent | Control access to the object | Add new behavior/responsibility | Convert interface to another |
| Interface | Same as subject | Same as component | Different from adaptee |
| Knows concrete class? | Often yes (creates it) | No (works with abstraction) | Yes (wraps specific adaptee) |
| Multiplicity | Usually one proxy per subject | Multiple decorators stackable | One adapter per adaptee |
| Lifecycle control | May manage creation/destruction | Never manages lifecycle | Never manages lifecycle |
| Typical use | Lazy load, access check, cache | Streams, middleware, UI borders | Legacy integration, API compat |
| Client awareness | Client unaware of proxy | Client unaware of decoration | Client uses new interface |
Key distinction: A Proxy controls whether you access the object. A Decorator controls what happens when you access it. An Adapter controls how you access it.
Advantages & Disadvantages
| Advantages | Disadvantages |
|---|---|
| Controls access without modifying the real subject | Adds a layer of indirection - increased complexity |
| Enables lazy initialization, saving resources | Response time increases due to extra delegation |
| Separates cross-cutting concerns cleanly | Proxy and real subject can drift out of sync if interface changes |
| Client code remains unchanged (same interface) | Identity issues: proxy != realSubject in equality checks |
| Can be combined with other patterns (Proxy + Flyweight) | Debugging is harder - stack traces pass through proxy layers |
| Enables transparent distributed computing | Dynamic proxies (reflection-based) have runtime overhead |
| Thread safety can be centralized in the proxy | Over-proxying leads to "onion architecture" anti-pattern |
Constraints & Edge Cases
1. Proxy Chain Performance
When multiple proxies wrap the same subject (logging → caching → protection → real), each adds a method call to the stack. In latency-sensitive paths, measure the cumulative overhead. Consider collapsing multiple concerns into a single proxy if profiling shows degradation.
2. Identity & Equality Issues
proxy == realSubject // FALSE - different object references
This breaks collections, caching by identity, and == checks. Solutions: override equals()/hashCode() in the proxy to delegate to the real subject, or use interface-level identity (e.g., entity ID).
3. Thread Safety in Caching Proxy
A naive caching proxy with a shared HashMap causes race conditions under concurrent access. Use ConcurrentHashMap, read-write locks, or synchronized blocks. Consider cache stampede protection (only one thread fetches on miss; others wait).
4. Proxy Lifecycle Management
If the proxy creates the real subject lazily, who destroys it? Ensure the proxy implements Closeable/AutoCloseable and releases the real subject's resources. In IoC containers, register the proxy's destroy method.
5. Serialization Pitfalls
Proxied objects may not serialize correctly. Hibernate proxies, for example, cause LazyInitializationException if serialized outside a session. Detach or eagerly fetch before serialization boundaries (API responses, message queues).
6. Final Classes & Methods
Languages like Java cannot proxy final classes with CGLIB. Kotlin classes are final by default. Ensure the real subject is open for subclassing or use interface-based proxies.
Interview Follow-ups
Q1: How does Java's java.lang.reflect.Proxy work internally?
Answer: Proxy.newProxyInstance() takes a ClassLoader, an array of interfaces, and an InvocationHandler. At runtime, it generates a synthetic class that implements all specified interfaces. Every method call on this synthetic class is dispatched to InvocationHandler.invoke(proxy, method, args). The handler can then add behavior (logging, access checks) and optionally delegate to the real object via method.invoke(realSubject, args). This avoids writing a proxy class per interface but incurs reflection overhead.
Q2: In a microservices architecture, how would you implement a resilient remote proxy?
Answer: The remote proxy should incorporate:
- Circuit Breaker (Hystrix/Resilience4j): Stops calling a failing service after threshold breaches.
- Retry with exponential backoff: Handles transient failures.
- Timeout: Prevents indefinite blocking.
- Fallback: Returns cached/default data when the circuit is open.
- Bulkhead: Isolates thread pools per downstream service.
The proxy encapsulates all this, exposing a clean interface to the caller. Libraries like Feign + Resilience4j implement this pattern out of the box.
Q3: What is the difference between a static proxy and a dynamic proxy?
Answer: A static proxy is a hand-written class that implements the subject interface and delegates to the real subject - known at compile time. A dynamic proxy is generated at runtime (Java Proxy, CGLIB, ByteBuddy) using reflection or bytecode manipulation. Static proxies are faster but require a new class per interface. Dynamic proxies are flexible but harder to debug and have reflection overhead.
Q4: How would you prevent cache stampede in a caching proxy? (Hint only)
Hint: Think about what happens when 1000 threads simultaneously find a cache miss for the same key. Consider a locking mechanism per cache key (not global) or a "probabilistic early expiration" strategy.
Q5: How does Spring decide between JDK dynamic proxy and CGLIB proxy? (Hint only)
Hint: Consider what happens when a bean implements an interface versus when it doesn't. Also look into proxyTargetClass=true and what Spring Boot defaults to since version 2.0.
Counter Questions to Ask Interviewer
Use these to demonstrate depth and clarify scope before answering proxy-related design questions:
-
"Is the real service local or remote?" - Determines whether you need a virtual/protection proxy or a remote proxy with network resilience.
-
"What are the latency requirements?" - Helps decide if proxy overhead is acceptable or if you need to inline the logic.
-
"Is the proxy for a single instance or a pool of instances?" - Affects whether the proxy manages lifecycle or just delegates.
-
"Should the proxy be transparent to the client or explicitly known?" - Determines if you use the same interface (classic proxy) or a wrapper with a different API (closer to adapter).
-
"Are there thread-safety requirements?" - Critical for caching and virtual proxies in concurrent environments.
-
"Is the system using an IoC container like Spring?" - If yes, you likely get proxy support for free via AOP; no need to hand-roll.
References & Whitepapers
- Gamma, E., Helm, R., Johnson, R., Vlissides, J. - Design Patterns: Elements of Reusable Object-Oriented Software (1994), Chapter 4: Structural Patterns - Proxy.
- Java Dynamic Proxies -
java.lang.reflect.Proxy- Official Oracle documentation. - Spring AOP Documentation - Spring Framework Reference: AOP Proxies.
- CGLIB (Code Generation Library) - Bytecode-based proxy generation for classes without interfaces.
- Martin Fowler - Patterns of Enterprise Application Architecture (2002) - Lazy Load pattern (Virtual Proxy variant).
- Hibernate ORM - Bytecode Enhancement and Proxy - How Hibernate implements lazy loading.
- Resilience4j - Circuit Breaker documentation - Resilient remote proxy implementation.
Related Topics
- Decorator Pattern - Adds behavior without controlling access
- Adapter Pattern - Converts interfaces rather than controlling access
- Facade Pattern - Simplifies a complex subsystem interface
- Flyweight Pattern - Shares objects to reduce memory; proxy can wrap flyweights
- Chain of Responsibility - Multiple handlers in sequence, similar to proxy chains
- Singleton Pattern - Proxy can enforce single-instance access
The Proxy pattern is deceptively simple in structure but extraordinarily versatile in application. Master the six proxy types, understand when each applies, and you will handle any interview question or system design scenario involving controlled access, lazy loading, or transparent distribution.