Definition
- Proxy pattern – a structural pattern that provides a surrogate or placeholder for another object to control access to it.
- Same interface – the proxy implements the same interface as the real object, so clients cannot tell the difference.
- Behaviour injection – the proxy can add behaviour before or after delegating to the real object, such as lazy creation, access checks, caching, or remote communication.
- Spring AOP – wraps beans in dynamically generated proxies that run cross-cutting concerns like transactions, security, and logging around actual method calls.
- JDK vs CGLIB – JDK dynamic proxies require an interface; CGLIB generates a subclass for concrete classes but cannot proxy final classes or methods.
Imagine Rajesh wants to speak to the senior loan officer at National Bank of India, but he talks to the officer's assistant first. The assistant checks Rajesh's appointment (access control), remembers answers to common questions so the officer is not disturbed twice (caching), only calls the officer when actually needed (lazy), and can even phone a branch in another city on Rajesh's behalf (remote). Rajesh thinks he is dealing with the officer's office directly, but the assistant is a proxy controlling and enhancing access.
Understanding the Concepts
What is the Proxy pattern?
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. The proxy implements the same interface as the real object, so the client cannot tell the difference, but the proxy can add behaviour before or after delegating.
package com.bank.service;
import java.math.BigDecimal;
public interface AccountService {
BigDecimal getBalance(String accountId);
void withdraw(String accountId, BigDecimal amount);
}
Why does the Proxy pattern exist?
Without a proxy, adding concerns like security, caching, or lazy initialisation means modifying the real class, which violates the Single Responsibility Principle. The proxy lets you layer concerns around the real object without touching its code.
In banking, Rajesh needs a balance check to be cached for performance, access-controlled for security, and logged for audit. Without a proxy, all three concerns would be tangled inside the real AccountService.
// Without proxy: tangled concerns in the real class (bad)
public class AccountServiceImpl implements AccountService {
public BigDecimal getBalance(String accountId) {
checkPermission(); // security concern
if (cache.has(accountId)) return cache.get(accountId); // caching concern
log("getBalance called"); // audit concern
// actual business logic buried under cross-cutting concerns
}
}
What are the types of Proxy?
There are five standard types, each adding a different concern.
Virtual proxy (lazy initialisation)
Defers creating an expensive real object until it is first actually needed. The proxy holds a null reference and creates the real object on the first method call.
package com.bank.proxy;
import com.bank.service.AccountService;
import com.bank.service.AccountServiceImpl;
import java.math.BigDecimal;
public class LazyAccountProxy implements AccountService {
private AccountService real;
private AccountService getReal() {
if (real == null) {
real = new AccountServiceImpl(); // expensive creation deferred
}
return real;
}
@Override
public BigDecimal getBalance(String accountId) {
return getReal().getBalance(accountId);
}
@Override
public void withdraw(String accountId, BigDecimal amount) {
getReal().withdraw(accountId, amount);
}
}
Protection proxy (access control)
Enforces authorisation before delegating. In banking, only an authorised teller or the account owner can call withdraw.
package com.bank.proxy;
import com.bank.service.AccountService;
import java.math.BigDecimal;
public class SecureAccountProxy implements AccountService {
private final AccountService real;
private final String currentUserRole;
public SecureAccountProxy(AccountService real, String currentUserRole) {
this.real = real;
this.currentUserRole = currentUserRole;
}
@Override
public BigDecimal getBalance(String accountId) {
return real.getBalance(accountId); // read is allowed for all
}
@Override
public void withdraw(String accountId, BigDecimal amount) {
if (!"TELLER".equals(currentUserRole)
&& !"MANAGER".equals(currentUserRole)) {
throw new SecurityException(
"Role " + currentUserRole + " cannot withdraw");
}
real.withdraw(accountId, amount);
}
}
Caching proxy
Remember the results of expensive operations and returns the cached value on repeat calls.
package com.bank.proxy;
import com.bank.service.AccountService;
import java.math.BigDecimal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class CachingAccountProxy implements AccountService {
private final AccountService real;
private final Map<String, BigDecimal> cache = new ConcurrentHashMap<>();
public CachingAccountProxy(AccountService real) {
this.real = real;
}
@Override
public BigDecimal getBalance(String accountId) {
return cache.computeIfAbsent(accountId, real::getBalance);
}
@Override
public void withdraw(String accountId, BigDecimal amount) {
real.withdraw(accountId, amount);
cache.remove(accountId); // invalidate stale entry
}
}
Remote proxy
Represents an object that lives in another address space, hiding network communication. Java RMI stubs are remote proxies.
Smart proxy
Adds extra actions like logging, reference counting, or lock management around access.
How do JDK dynamic proxies work?
A JDK dynamic proxy is generated at runtime using java.lang.reflect.Proxy.newProxyInstance. It requires the target to implement at least one interface and routes every method call through an InvocationHandler.
package com.bank.proxy;
import com.bank.service.AccountService;
import com.bank.service.AccountServiceImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.logging.Logger;
public class LoggingProxyFactory {
private static final Logger log = Logger.getLogger("AuditProxy");
@SuppressWarnings("unchecked")
public static <T> T create(T target, Class<T> iface) {
return (T) Proxy.newProxyInstance(
iface.getClassLoader(),
new Class[]{iface},
(proxy, method, args) -> {
log.info("Before: " + method.getName());
Object result = method.invoke(target, args);
log.info("After: " + method.getName());
return result;
}
);
}
public static void main(String[] args) {
AccountService real = new AccountServiceImpl();
AccountService proxied = create(real, AccountService.class);
proxied.getBalance("SAV2001");
}
}
Before: getBalance
After: getBalance
How does CGLIB differ from JDK proxies?
CGLIB generates a runtime subclass of the target class, so it works on concrete classes without interfaces. However, it cannot proxy final classes or final methods because subclassing is impossible for them.
| Aspect | JDK Dynamic Proxy | CGLIB |
|---|---|---|
| Requirement | Target must implement an interface | No interface needed |
| Mechanism | Implements interface at runtime | Generates subclass at runtime |
| Final classes | Works (proxies the interface) | Cannot proxy final classes |
| Final methods | N/A (interface methods) | Cannot override final methods |
| Performance | Slightly faster for interface calls | Slightly slower due to subclassing |
| Used by | Spring default when interface exists | Spring fallback when no interface |
How does Spring AOP use dynamic proxies?
When you annotate a method with @Transactional, @Cacheable, or @Async, Spring does not modify your class. At startup, Spring wraps your bean in a proxy.
- If your bean implements an interface, Spring creates a JDK dynamic proxy.
- If your bean has no interface, Spring uses CGLIB to create a subclass proxy.
When another bean calls your method, it really calls the proxy. The proxy runs the advice around your method.
package com.bank.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class TransferService {
@Transactional
public void transfer(String fromId, String toId, BigDecimal amount) {
// Spring proxy opens a transaction BEFORE this method
accountRepo.debit(fromId, amount);
accountRepo.credit(toId, amount);
// Spring proxy commits on success, rolls back on exception
}
}
What is the self-invocation problem?
Because the proxy wraps the bean from outside, a call from one method of a bean to another method of the same bean using this.otherMethod() bypasses the proxy entirely. Annotations like @Transactional on the inner method silently do nothing.
@Service
public class LoanService {
public void processLoan(String loanId) {
// This call bypasses the proxy, so @Transactional does NOT apply
this.approveLoan(loanId);
}
@Transactional
public void approveLoan(String loanId) {
// Transaction is NOT active because the proxy was bypassed
}
}
The fixes are to call the annotated method from another bean, to inject the bean into itself, or to use AopContext.currentProxy().
What are common mistakes with the Proxy pattern?
The first mistake is not understanding that JDK proxies require interfaces. If you remove the interface from a bean, Spring silently switches to CGLIB, which can break if the class is final.
// Wrong: final class cannot be proxied by CGLIB
@Service
public final class AccountService { // Spring cannot create a CGLIB proxy
@Transactional
public void debit(String id, BigDecimal amount) { }
}
The second mistake is confusing Proxy with Decorator. Both wrap an object, but their intent differs: Proxy controls access, Decorator adds behaviour.
What should a senior engineer know?
A senior engineer understands that most Spring "magic" is just the Proxy pattern. @Transactional, @Cacheable, @Async, and @Secured all work by wrapping beans in proxies. Understanding this explains why self-invocation breaks annotations, why final classes cause errors, and why proxied methods must be public.
In production at National Bank of India, Pankaj wraps the account service in a dynamic proxy that adds a short-lived balance cache and a role-based access check, so repeated balance lookups are served from cache and only a teller can withdraw, all without editing the real service.
// Composing proxies: cache + security + real service
AccountService real = new AccountServiceImpl();
AccountService cached = new CachingAccountProxy(real);
AccountService secured = new SecureAccountProxy(cached, "TELLER");
secured.withdraw("SAV2001", new BigDecimal("5000.00"));
Visual Overview
Client Call
│
▼
┌─────────────────────────────────┐
│ Proxy Object │
│ (same interface as real) │
│ │
│ Before: security / cache check │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Real Object │ │
│ │ (actual business) │ │
│ └───────────────────────┘ │
│ │ │
│ After: log / commit / cache │
└─────────────────────────────────┘
│
▼
Response to Client
Spring AOP Proxy Flow
─────────────────────
Bean A ──calls──→ Proxy(Bean B)
│
┌─────┴─────┐
│ Open Txn │ (Before advice)
│ Delegate │──→ Real Bean B method
│ Commit/ │ (After advice)
│ Rollback │
└───────────┘
Self-invocation problem:
Bean B.method1() ──this.method2()──→ Real method2()
(bypasses proxy, no advice runs)
Key Takeaway
The Proxy pattern provides a stand-in for another object to control access to it. The proxy implements the same interface as the real object, so clients cannot tell they are talking to a surrogate, but the proxy can add behaviour before or after delegating. There are several standard types: a virtual proxy lazily creates an expensive object only when first needed, a protection proxy enforces access control before delegating, a caching proxy returns cached results on repeat calls, and a remote proxy represents an object in another process and hides the networking.
In Java, proxies come in two forms. A static proxy is a class you write by hand. A dynamic proxy is generated at runtime: the JDK dynamic proxy uses Proxy.newProxyInstance with an InvocationHandler, routing every method call through an invoke method where you add behaviour and then delegate. The key limitation is that JDK dynamic proxies need the target to implement an interface. When there is no interface, Spring falls back to CGLIB, which generates a subclass, so it cannot proxy final classes or methods.
This is exactly how Spring AOP works. When Rajesh annotates a method with @Transactional, @Cacheable, or @Async, Spring does not change his class. At startup it wraps his bean in a proxy, which runs the advice around his method. For @Transactional it opens a transaction, commits on success, and rolls back on an exception, then delegates to the real method. The business code stays clean and the cross-cutting concern is added by the proxy.
The one trap to always warn about is self-invocation: if a bean calls another of its own methods with plain this, the call does not go through the proxy, so a @Transactional annotation on that inner method silently does nothing. The fix is to call the annotated method from a different bean, inject the bean into itself, or use AopContext.currentProxy(). Understanding this explains a whole class of confusing Spring bugs.
Remember one sentence: a Proxy is a same-interface stand-in that controls access, and Spring AOP is the Proxy pattern applied to cross-cutting concerns like transactions and caching.
---