Proxy Design Pattern
1. What is Proxy Pattern?
The Proxy pattern is a GOF Structural Pattern. A Proxy represents a dummy acting as real with additional functionalities. It provides a surrogate or placeholder for another object to control access to it.
One line: The proxy object gives the feeling of working with the real object, but adds additional functionalities (security, caching, logging) that are not in the original implementation.
Real-world analogies
- Film industry: A stunt double (proxy) acts in place of the real actor for dangerous scenes. The audience sees the same character, but the actual work is done by the proxy with additional safety measures.
- Embassy: The embassy acts as a proxy for the home country government. You interact with the embassy (proxy) instead of going to the actual government offices.
- ATM: The ATM is a proxy for the bank. You interact with the ATM, which controls access to your bank account with security checks.
- Internet Proxy Server: Controls access to websites, caches content, logs activity.
Three types of Proxy
| Type | Purpose | Example |
|---|---|---|
| Protection Proxy | Controls access based on permissions/authorization. Adds security layer before the real object. | Bank service proxy checks if user is authorized before allowing withdrawal |
| Virtual Proxy | Lazy loading. Creates the expensive real object only when actually needed. | Image proxy loads the actual image only when display() is called |
| Remote Proxy | Represents an object in a different address space (different JVM, different server). | RMI stub acts as local proxy for a remote object |
2. The Problem
When clients directly access a service object, there is no way to add cross-cutting concerns like:
- Authentication and authorization checks before method calls
- Logging of all method calls for auditing
- Caching responses to avoid repeated expensive operations
- Lazy initialization of heavy resources
- Rate limiting to prevent abuse
Adding these concerns directly to the service class violates Single Responsibility Principle. The service should focus on business logic, not security or logging.
3. Protection Proxy: Bank Service Example
A BankService has methods to check balance and withdraw money. The proxy adds authorization: only authenticated users with the correct role can access these methods. The real BankService has no security logic; the proxy handles it.
BankService.java (interface)
package com.nt.service;
public interface BankService {
String getBalance(String acno, float amount);
String addBalance(String acno, float amount);
}
BankServiceImpl.java (real implementation)
package com.nt.service;
public class BankServiceImpl implements BankService {
private float balance;
public BankServiceImpl() {
this.balance = 50000.0f; // initial balance
System.out.println("BankServiceImpl created (real service)");
}
@Override
public String getBalance(String acno, float amount) {
return "Account: " + acno + " | Balance: Rs " + balance;
}
@Override
public String addBalance(String acno, float amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance += amount;
return "Deposited Rs " + amount + " to " + acno + ". New balance: Rs " + balance;
}
}
ProxyBankService.java (protection proxy)
package com.nt.proxy;
import com.nt.service.BankService;
import com.nt.service.BankServiceImpl;
public class ProxyBankService implements BankService {
private BankService realService;
private String authenticatedUser;
private String role;
public ProxyBankService(String user, String role) {
this.authenticatedUser = user;
this.role = role;
// Real service created lazily (Virtual Proxy aspect)
}
@Override
public String getBalance(String acno, float amount) {
// Protection: check authorization
if (authenticatedUser == null) {
throw new SecurityException("Not authenticated");
}
// Lazy init real service (Virtual Proxy)
if (realService == null) {
realService = new BankServiceImpl();
}
System.out.println("[PROXY] " + authenticatedUser + " accessing balance");
return realService.getBalance(acno, amount);
}
@Override
public String addBalance(String acno, float amount) {
// Protection: only ADMIN can add balance
if (!"ADMIN".equalsIgnoreCase(role)) {
throw new SecurityException(
"Access denied: only ADMIN can add balance. Your role: " + role);
}
if (realService == null) {
realService = new BankServiceImpl();
}
System.out.println("[PROXY] ADMIN " + authenticatedUser + " adding balance");
return realService.addBalance(acno, amount);
}
}
BankServiceFactory.java
package com.nt.factory;
import com.nt.service.BankService;
import com.nt.proxy.ProxyBankService;
public class BankServiceFactory {
public static BankService getService(String user, String role) {
return new ProxyBankService(user, role);
}
}
Client test
package com.nt.test;
import com.nt.service.BankService;
import com.nt.factory.BankServiceFactory;
public class ProxyDPTest {
public static void main(String[] args) {
// Scenario 1: Authorized user checks balance
System.out.println("=== Scenario 1: User checks balance ===");
BankService userService = BankServiceFactory.getService("Rajesh", "USER");
System.out.println(userService.getBalance("SAV2001", 0));
System.out.println();
// Scenario 2: Admin adds balance
System.out.println("=== Scenario 2: Admin adds balance ===");
BankService adminService = BankServiceFactory.getService("Priya", "ADMIN");
System.out.println(adminService.addBalance("SAV2001", 25000));
System.out.println();
// Scenario 3: User tries to add balance (denied)
System.out.println("=== Scenario 3: User tries admin operation ===");
try {
userService.addBalance("SAV2001", 10000);
} catch (SecurityException e) {
System.out.println("BLOCKED: " + e.getMessage());
}
}
}
4. Two Ways to Generate Proxy Classes
| Approach | Description | When to use |
|---|---|---|
| a) Manual Proxy | Write a proxy class by hand that implements the same interface. You code the additional logic explicitly. | Simple cases, few methods, full control |
| b) Dynamic Proxy (InMemory) | Generate proxy class at runtime using CGLIB or JDK libraries. No manual proxy class needed. | Many methods, cross-cutting concerns (AOP), frameworks |
Normal Java class vs InMemory Dynamic class
5. JDK Dynamic Proxy (InvocationHandler)
JDK Dynamic Proxy uses java.lang.reflect.Proxy and InvocationHandler. It generates a proxy class at runtime that implements the specified interfaces. Works only with interface-based proxying.
package com.nt.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.nt.service.BankService;
import com.nt.service.BankServiceImpl;
// InvocationHandler: intercepts all method calls on the proxy
public class BankServiceInvocationHandler implements InvocationHandler {
private final BankService realService;
private final String user;
public BankServiceInvocationHandler(String user) {
this.realService = new BankServiceImpl();
this.user = user;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Before: logging
System.out.println("[JDK PROXY] " + user + " calling " + method.getName());
// Before: check if method is addBalance, require ADMIN
if (method.getName().equals("addBalance")) {
System.out.println("[JDK PROXY] Authorization check for addBalance");
}
// Invoke the real method
Object result = method.invoke(realService, args);
// After: logging
System.out.println("[JDK PROXY] " + method.getName() + " completed");
return result;
}
}
// Factory to create JDK Dynamic Proxy
package com.nt.factory;
import java.lang.reflect.Proxy;
import com.nt.service.BankService;
import com.nt.proxy.BankServiceInvocationHandler;
public class JdkProxyFactory {
public static BankService createProxy(String user) {
BankServiceInvocationHandler handler = new BankServiceInvocationHandler(user);
return (BankService) Proxy.newProxyInstance(
BankService.class.getClassLoader(),
new Class[]{BankService.class}, // must be interface(s)
handler
);
}
}
// Client
BankService proxy = JdkProxyFactory.createProxy("Rajesh");
System.out.println(proxy.getBalance("SAV2001", 0));
6. CGLIB Dynamic Proxy (MethodInterceptor)
CGLIB (Code Generation Library) generates proxy classes by creating a subclass of the target class at runtime. Works with both classes and interfaces. Does not require the target to implement an interface. Used internally by Spring AOP, Mockito, and Hibernate.
// Step 1: Add CGLIB jars to classpath
// cglib-3.3.0.jar (https://mvnrepository.com/artifact/cglib/cglib/3.3.0)
// asm-7.1.jar (https://mvnrepository.com/artifact/org.ow2.asm/asm/7.1)
// Step 2: Create MethodInterceptor
package com.nt.proxy;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class BankServiceMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
// Before
System.out.println("[CGLIB PROXY] Intercepting: " + method.getName());
// Check: we can validate even more than 4000 lines for the protection
if (method.getName().equals("addBalance")) {
System.out.println("[CGLIB PROXY] Admin authorization required");
}
// Call the real method
Object result = methodProxy.invokeSuper(obj, args);
// After
System.out.println("[CGLIB PROXY] " + method.getName() + " completed");
return result;
}
}
// Step 3: Create proxy using Enhancer
package com.nt.factory;
import net.sf.cglib.proxy.Enhancer;
import com.nt.service.BankServiceImpl;
import com.nt.proxy.BankServiceMethodInterceptor;
public class CglibProxyFactory {
public static BankServiceImpl createProxy() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(BankServiceImpl.class); // target class (no interface needed)
enhancer.setCallback(new BankServiceMethodInterceptor());
return (BankServiceImpl) enhancer.create(); // generate subclass proxy
}
}
JDK Proxy vs CGLIB Proxy
| Aspect | JDK Dynamic Proxy | CGLIB Proxy |
|---|---|---|
| Mechanism | Implements interfaces at runtime | Creates subclass at runtime |
| Requires interface? | Yes (must proxy an interface) | No (can proxy concrete classes) |
| Library | Built into JDK (java.lang.reflect.Proxy) | External library (cglib.jar + asm.jar) |
| Performance | Slightly slower (reflection-based) | Faster (bytecode generation) |
| Cannot proxy | Concrete classes without interface | Final classes, final methods |
| Used by | Spring AOP (interface-based beans) | Spring AOP (class-based beans), Mockito, Hibernate |
7. Connection to Spring AOP
Spring AOP (Aspect-Oriented Programming) is built entirely on the Proxy pattern. When you add @Transactional, @Cacheable, @Secured, or custom aspects, Spring creates a proxy around your bean. The proxy intercepts method calls and adds the cross-cutting concern before/after the real method.
- If the bean implements an interface: Spring uses JDK Dynamic Proxy
- If the bean is a concrete class: Spring uses CGLIB Proxy
Mocking frameworks like Mockito also use CGLIB/ByteBuddy to generate proxies for the classes being mocked.
// Spring @Transactional creates a proxy around the service
@Service
public class TransferService {
@Transactional // Spring creates a proxy that wraps this method
public void transfer(String from, String to, BigDecimal amount) {
// Spring proxy: BEGIN TRANSACTION (before)
accountRepository.debit(from, amount);
accountRepository.credit(to, amount);
// Spring proxy: COMMIT TRANSACTION (after)
// Spring proxy: ROLLBACK on exception
}
}
// What Spring does internally:
// TransferService proxy = CglibProxyFactory.createProxy(TransferService.class);
// proxy adds: begin transaction -> call real method -> commit/rollback
8. Full-Length Industry Project: API Gateway with Proxy
National Bank of India's microservices architecture needs an API Gateway that sits in front of backend services. The gateway acts as a proxy that adds: authentication (protection proxy), request logging (logging proxy), response caching (virtual proxy), and rate limiting. Backend services contain only business logic; the gateway handles all cross-cutting concerns.
High-Level Design (HLD)
Low-Level Design (LLD)
Project Structure
BankingService.java (interface)
package com.nbi.gateway.service;
import com.nbi.gateway.model.AccountResponse;
import com.nbi.gateway.model.TransferResponse;
import java.math.BigDecimal;
import java.util.List;
public interface BankingService {
AccountResponse getAccountBalance(String accountNo);
TransferResponse transfer(String from, String to, BigDecimal amount);
List<String> getTransactionHistory(String accountNo);
}
Model classes
package com.nbi.gateway.model;
import java.math.BigDecimal;
public class AccountResponse {
private final String accountNo;
private final BigDecimal balance;
private final String holderName;
public AccountResponse(String accountNo, BigDecimal balance, String holderName) {
this.accountNo = accountNo;
this.balance = balance;
this.holderName = holderName;
}
public String getAccountNo() { return accountNo; }
public BigDecimal getBalance() { return balance; }
@Override
public String toString() {
return "Account: " + accountNo + " | Holder: " + holderName
+ " | Balance: Rs " + balance;
}
}
public class TransferResponse {
private final boolean success;
private final String transactionId;
private final String message;
public TransferResponse(boolean success, String transactionId, String message) {
this.success = success;
this.transactionId = transactionId;
this.message = message;
}
public boolean isSuccess() { return success; }
@Override
public String toString() {
return (success ? "SUCCESS" : "FAILED") + " | " + transactionId + " | " + message;
}
}
RealBankingService.java
package com.nbi.gateway.service;
import com.nbi.gateway.model.AccountResponse;
import com.nbi.gateway.model.TransferResponse;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class RealBankingService implements BankingService {
public RealBankingService() {
System.out.println("[REAL SERVICE] BankingService initialized");
}
@Override
public AccountResponse getAccountBalance(String accountNo) {
System.out.println("[REAL SERVICE] Querying database for " + accountNo);
// Simulate DB query
return new AccountResponse(accountNo, new BigDecimal("250000.00"), "Rajesh Kumar");
}
@Override
public TransferResponse transfer(String from, String to, BigDecimal amount) {
System.out.println("[REAL SERVICE] Processing transfer: " + from + " -> " + to);
String txnId = "TXN-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
return new TransferResponse(true, txnId,
"Transferred Rs " + amount + " from " + from + " to " + to);
}
@Override
public List<String> getTransactionHistory(String accountNo) {
System.out.println("[REAL SERVICE] Fetching history for " + accountNo);
return Arrays.asList(
"2026-11-01 | CREDIT | Rs 50,000 | Salary",
"2026-11-01 | DEBIT | Rs 5,000 | UPI Transfer",
"2026-10-30 | DEBIT | Rs 2,500 | Bill Payment"
);
}
}
BankingServiceProxy.java (Proxy with all concerns)
package com.nbi.gateway.proxy;
import com.nbi.gateway.model.AccountResponse;
import com.nbi.gateway.model.TransferResponse;
import com.nbi.gateway.service.BankingService;
import com.nbi.gateway.service.RealBankingService;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public class BankingServiceProxy implements BankingService {
private BankingService realService; // lazy init (Virtual Proxy)
private final String authToken;
private final Map<String, Object> cache = new HashMap<>();
private final AtomicInteger requestCount = new AtomicInteger(0);
private static final int MAX_REQUESTS_PER_MINUTE = 10;
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
public BankingServiceProxy(String authToken) {
this.authToken = authToken;
}
@Override
public AccountResponse getAccountBalance(String accountNo) {
String method = "getAccountBalance";
// 1. Authentication (Protection Proxy)
authenticate(method);
// 2. Rate limiting (Protection Proxy)
checkRateLimit(method);
// 3. Check cache (Virtual Proxy / Caching Proxy)
String cacheKey = "balance_" + accountNo;
if (cache.containsKey(cacheKey)) {
log(method, "CACHE HIT for " + accountNo);
return (AccountResponse) cache.get(cacheKey);
}
// 4. Log request
log(method, "Request for " + accountNo);
// 5. Lazy init + delegate to real service
AccountResponse response = getRealService().getAccountBalance(accountNo);
// 6. Cache response
cache.put(cacheKey, response);
log(method, "Response cached for " + accountNo);
// 7. Log response
log(method, "Response: " + response);
return response;
}
@Override
public TransferResponse transfer(String from, String to, BigDecimal amount) {
String method = "transfer";
authenticate(method);
checkRateLimit(method);
log(method, "Transfer request: " + from + " -> " + to + " Rs " + amount);
// Transfers are never cached
TransferResponse response = getRealService().transfer(from, to, amount);
// Invalidate balance cache for both accounts
cache.remove("balance_" + from);
cache.remove("balance_" + to);
log(method, "Balance cache invalidated for " + from + " and " + to);
log(method, "Response: " + response);
return response;
}
@Override
public List<String> getTransactionHistory(String accountNo) {
String method = "getTransactionHistory";
authenticate(method);
checkRateLimit(method);
log(method, "History request for " + accountNo);
List<String> history = getRealService().getTransactionHistory(accountNo);
log(method, "Returned " + history.size() + " transactions");
return history;
}
// Protection Proxy: authenticate
private void authenticate(String method) {
if (authToken == null || authToken.isEmpty()) {
log(method, "BLOCKED - No auth token");
throw new SecurityException("Authentication required. No token provided.");
}
if (!authToken.startsWith("Bearer ")) {
log(method, "BLOCKED - Invalid token format");
throw new SecurityException("Invalid token format. Expected: Bearer ");
}
log(method, "Authenticated with token: " + authToken.substring(0, 15) + "...");
}
// Protection Proxy: rate limiting
private void checkRateLimit(String method) {
int count = requestCount.incrementAndGet();
if (count > MAX_REQUESTS_PER_MINUTE) {
log(method, "RATE LIMITED - " + count + "/" + MAX_REQUESTS_PER_MINUTE);
throw new RuntimeException("Rate limit exceeded. Max " + MAX_REQUESTS_PER_MINUTE
+ " requests per minute. Try again later.");
}
log(method, "Rate: " + count + "/" + MAX_REQUESTS_PER_MINUTE);
}
// Virtual Proxy: lazy initialization of real service
private BankingService getRealService() {
if (realService == null) {
realService = new RealBankingService();
}
return realService;
}
private void log(String method, String message) {
String timestamp = LocalDateTime.now().format(FORMATTER);
System.out.println("[" + timestamp + "] [PROXY:" + method + "] " + message);
}
}
Application.java
package com.nbi.gateway;
import com.nbi.gateway.model.AccountResponse;
import com.nbi.gateway.model.TransferResponse;
import com.nbi.gateway.proxy.BankingServiceProxy;
import com.nbi.gateway.service.BankingService;
import java.math.BigDecimal;
import java.util.List;
public class Application {
public static void main(String[] args) {
// Scenario 1: Authenticated user checks balance
System.out.println("=== Scenario 1: Check Balance ===");
BankingService service = new BankingServiceProxy("Bearer jwt_token_abc123");
AccountResponse balance = service.getAccountBalance("SAV2001");
System.out.println("Result: " + balance);
System.out.println();
// Scenario 2: Second call hits cache
System.out.println("=== Scenario 2: Cache Hit ===");
AccountResponse cached = service.getAccountBalance("SAV2001");
System.out.println("Result: " + cached);
System.out.println();
// Scenario 3: Transfer (invalidates cache)
System.out.println("=== Scenario 3: Transfer ===");
TransferResponse transfer = service.transfer("SAV2001", "CUR3001",
new BigDecimal("50000"));
System.out.println("Result: " + transfer);
System.out.println();
// Scenario 4: Balance after transfer (cache was invalidated, fetches fresh)
System.out.println("=== Scenario 4: Fresh Balance After Transfer ===");
AccountResponse freshBalance = service.getAccountBalance("SAV2001");
System.out.println("Result: " + freshBalance);
System.out.println();
// Scenario 5: Transaction history
System.out.println("=== Scenario 5: Transaction History ===");
List<String> history = service.getTransactionHistory("SAV2001");
history.forEach(t -> System.out.println(" " + t));
System.out.println();
// Scenario 6: No authentication (blocked)
System.out.println("=== Scenario 6: No Auth Token ===");
try {
BankingService noAuth = new BankingServiceProxy("");
noAuth.getAccountBalance("SAV2001");
} catch (SecurityException e) {
System.out.println("BLOCKED: " + e.getMessage());
}
System.out.println();
// Scenario 7: Rate limiting
System.out.println("=== Scenario 7: Rate Limiting ===");
BankingService rateLimitTest = new BankingServiceProxy("Bearer test_token");
for (int i = 1; i <= 12; i++) {
try {
rateLimitTest.getAccountBalance("SAV2001");
} catch (RuntimeException e) {
System.out.println("Request " + i + ": " + e.getMessage());
}
}
}
}
Expected Output
Key observations from the project
- Four proxy concerns in one class: Authentication (protection), rate limiting (protection), caching (virtual), logging (logging proxy). The real service has zero knowledge of these concerns.
- Lazy initialization: The
RealBankingServiceis created only on the first actual method call (scenario 1). Before that, only the proxy exists in memory. - Cache with invalidation:
getAccountBalancecaches the response.transferinvalidates the cache for affected accounts. The next balance call fetches fresh data. - Rate limiting with AtomicInteger: Thread-safe counter. After 10 requests, further calls are blocked with a clear error message.
- Same interface:
BankingServiceProxyimplementsBankingService. The client works with the interface and does not know it is talking to a proxy. - Seven scenarios cover: Normal flow, cache hit, transfer with cache invalidation, fresh fetch, history, auth failure, rate limiting.
9. When to Use and When NOT to Use
When to use Proxy
| Scenario | Proxy Type | Example |
|---|---|---|
| Access control / security | Protection Proxy | Check permissions before allowing method call |
| Lazy loading of expensive objects | Virtual Proxy | Load image/data only when displayed |
| Caching responses | Caching Proxy | Return cached result for repeated queries |
| Logging and auditing | Logging Proxy | Log every method call with timestamp |
| Rate limiting | Protection Proxy | Limit API calls per client per minute |
| Remote object access | Remote Proxy | RMI stub, gRPC client, REST client wrapper |
| Spring AOP | Dynamic Proxy | @Transactional, @Cacheable, @Secured |
When NOT to use Proxy
| Scenario | Why NOT | Use Instead |
|---|---|---|
| No cross-cutting concerns | Proxy adds indirection for no benefit | Direct service call |
| Simple decorator needed | If only adding behaviour, Decorator is simpler | Decorator pattern |
| Performance-critical path | Proxy adds method call overhead | Direct call or inline logic |
Proxy vs Decorator vs Adapter
| Aspect | Proxy | Decorator | Adapter |
|---|---|---|---|
| Purpose | Control access to the object | Add responsibilities to the object | Convert interface to another interface |
| Same interface? | Yes (same as real object) | Yes (same as wrapped object) | No (converts to target interface) |
| Knows real object? | Yes (creates or holds reference) | No (wraps any component) | Yes (wraps the adaptee) |
| Stackable? | Usually single proxy | Multiple decorators stackable | Usually single adapter |
10. Quick Reference Card
| Component | Role | In API Gateway Project |
|---|---|---|
| Subject Interface | Common interface for real and proxy | BankingService |
| Real Subject | The actual implementation with business logic | RealBankingService |
| Proxy | Controls access, adds concerns, delegates to real | BankingServiceProxy |
| Client | Uses the interface, unaware of proxy | Application |
Proxy Design Pattern in Java
1. What is Proxy Pattern?
The Proxy pattern is a GOF Structural Pattern. A Proxy represents a dummy acting as real with additional functionalities. It provides a surrogate or placeholder for another object to control access to it.
One line: The proxy object gives the feeling of working with the real object, but adds additional functionalities (security, caching, logging) that are not in the original implementation.
Real-world analogies
- Film industry: A stunt double (proxy) acts in place of the real actor for dangerous scenes. The audience sees the same character, but the actual work is done by the proxy with additional safety measures.
- Embassy: The embassy acts as a proxy for the home country government. You interact with the embassy (proxy) instead of going to the actual government offices.
- ATM: The ATM is a proxy for the bank. You interact with the ATM, which controls access to your bank account with security checks.
- Internet Proxy Server: Controls access to websites, caches content, logs activity.
Three types of Proxy
| Type | Purpose | Example |
|---|---|---|
| Protection Proxy | Controls access based on permissions/authorization. Adds security layer before the real object. | Bank service proxy checks if user is authorized before allowing withdrawal |
| Virtual Proxy | Lazy loading. Creates the expensive real object only when actually needed. | Image proxy loads the actual image only when display() is called |
| Remote Proxy | Represents an object in a different address space (different JVM, different server). | RMI stub acts as local proxy for a remote object |
2. The Problem
When clients directly access a service object, there is no way to add cross-cutting concerns like:
- Authentication and authorization checks before method calls
- Logging of all method calls for auditing
- Caching responses to avoid repeated expensive operations
- Lazy initialization of heavy resources
- Rate limiting to prevent abuse
Adding these concerns directly to the service class violates Single Responsibility Principle. The service should focus on business logic, not security or logging.
3. Protection Proxy: Bank Service Example
A BankService has methods to check balance and withdraw money. The proxy adds authorization: only authenticated users with the correct role can access these methods. The real BankService has no security logic; the proxy handles it.
BankService.java (interface)
package com.nt.service;
public interface BankService {
String getBalance(String acno, float amount);
String addBalance(String acno, float amount);
}
BankServiceImpl.java (real implementation)
package com.nt.service;
public class BankServiceImpl implements BankService {
private float balance;
public BankServiceImpl() {
this.balance = 50000.0f; // initial balance
System.out.println("BankServiceImpl created (real service)");
}
@Override
public String getBalance(String acno, float amount) {
return "Account: " + acno + " | Balance: Rs " + balance;
}
@Override
public String addBalance(String acno, float amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance += amount;
return "Deposited Rs " + amount + " to " + acno + ". New balance: Rs " + balance;
}
}
ProxyBankService.java (protection proxy)
package com.nt.proxy;
import com.nt.service.BankService;
import com.nt.service.BankServiceImpl;
public class ProxyBankService implements BankService {
private BankService realService;
private String authenticatedUser;
private String role;
public ProxyBankService(String user, String role) {
this.authenticatedUser = user;
this.role = role;
// Real service created lazily (Virtual Proxy aspect)
}
@Override
public String getBalance(String acno, float amount) {
// Protection: check authorization
if (authenticatedUser == null) {
throw new SecurityException("Not authenticated");
}
// Lazy init real service (Virtual Proxy)
if (realService == null) {
realService = new BankServiceImpl();
}
System.out.println("[PROXY] " + authenticatedUser + " accessing balance");
return realService.getBalance(acno, amount);
}
@Override
public String addBalance(String acno, float amount) {
// Protection: only ADMIN can add balance
if (!"ADMIN".equalsIgnoreCase(role)) {
throw new SecurityException(
"Access denied: only ADMIN can add balance. Your role: " + role);
}
if (realService == null) {
realService = new BankServiceImpl();
}
System.out.println("[PROXY] ADMIN " + authenticatedUser + " adding balance");
return realService.addBalance(acno, amount);
}
}
BankServiceFactory.java
package com.nt.factory;
import com.nt.service.BankService;
import com.nt.proxy.ProxyBankService;
public class BankServiceFactory {
public static BankService getService(String user, String role) {
return new ProxyBankService(user, role);
}
}
Client test
package com.nt.test;
import com.nt.service.BankService;
import com.nt.factory.BankServiceFactory;
public class ProxyDPTest {
public static void main(String[] args) {
// Scenario 1: Authorized user checks balance
System.out.println("=== Scenario 1: User checks balance ===");
BankService userService = BankServiceFactory.getService("Rajesh", "USER");
System.out.println(userService.getBalance("SAV2001", 0));
System.out.println();
// Scenario 2: Admin adds balance
System.out.println("=== Scenario 2: Admin adds balance ===");
BankService adminService = BankServiceFactory.getService("Priya", "ADMIN");
System.out.println(adminService.addBalance("SAV2001", 25000));
System.out.println();
// Scenario 3: User tries to add balance (denied)
System.out.println("=== Scenario 3: User tries admin operation ===");
try {
userService.addBalance("SAV2001", 10000);
} catch (SecurityException e) {
System.out.println("BLOCKED: " + e.getMessage());
}
}
}
4. Two Ways to Generate Proxy Classes
| Approach | Description | When to use |
|---|---|---|
| a) Manual Proxy | Write a proxy class by hand that implements the same interface. You code the additional logic explicitly. | Simple cases, few methods, full control |
| b) Dynamic Proxy (InMemory) | Generate proxy class at runtime using CGLIB or JDK libraries. No manual proxy class needed. | Many methods, cross-cutting concerns (AOP), frameworks |
Normal Java class vs InMemory Dynamic class
5. JDK Dynamic Proxy (InvocationHandler)
JDK Dynamic Proxy uses java.lang.reflect.Proxy and InvocationHandler. It generates a proxy class at runtime that implements the specified interfaces. Works only with interface-based proxying.
package com.nt.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.nt.service.BankService;
import com.nt.service.BankServiceImpl;
// InvocationHandler: intercepts all method calls on the proxy
public class BankServiceInvocationHandler implements InvocationHandler {
private final BankService realService;
private final String user;
public BankServiceInvocationHandler(String user) {
this.realService = new BankServiceImpl();
this.user = user;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Before: logging
System.out.println("[JDK PROXY] " + user + " calling " + method.getName());
// Before: check if method is addBalance, require ADMIN
if (method.getName().equals("addBalance")) {
System.out.println("[JDK PROXY] Authorization check for addBalance");
}
// Invoke the real method
Object result = method.invoke(realService, args);
// After: logging
System.out.println("[JDK PROXY] " + method.getName() + " completed");
return result;
}
}
// Factory to create JDK Dynamic Proxy
package com.nt.factory;
import java.lang.reflect.Proxy;
import com.nt.service.BankService;
import com.nt.proxy.BankServiceInvocationHandler;
public class JdkProxyFactory {
public static BankService createProxy(String user) {
BankServiceInvocationHandler handler = new BankServiceInvocationHandler(user);
return (BankService) Proxy.newProxyInstance(
BankService.class.getClassLoader(),
new Class[]{BankService.class}, // must be interface(s)
handler
);
}
}
// Client
BankService proxy = JdkProxyFactory.createProxy("Rajesh");
System.out.println(proxy.getBalance("SAV2001", 0));
6. CGLIB Dynamic Proxy (MethodInterceptor)
CGLIB (Code Generation Library) generates proxy classes by creating a subclass of the target class at runtime. Works with both classes and interfaces. Does not require the target to implement an interface. Used internally by Spring AOP, Mockito, and Hibernate.
// Step 1: Add CGLIB jars to classpath
// cglib-3.3.0.jar (https://mvnrepository.com/artifact/cglib/cglib/3.3.0)
// asm-7.1.jar (https://mvnrepository.com/artifact/org.ow2.asm/asm/7.1)
// Step 2: Create MethodInterceptor
package com.nt.proxy;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class BankServiceMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
// Before
System.out.println("[CGLIB PROXY] Intercepting: " + method.getName());
// Check: we can validate even more than 4000 lines for the protection
if (method.getName().equals("addBalance")) {
System.out.println("[CGLIB PROXY] Admin authorization required");
}
// Call the real method
Object result = methodProxy.invokeSuper(obj, args);
// After
System.out.println("[CGLIB PROXY] " + method.getName() + " completed");
return result;
}
}
// Step 3: Create proxy using Enhancer
package com.nt.factory;
import net.sf.cglib.proxy.Enhancer;
import com.nt.service.BankServiceImpl;
import com.nt.proxy.BankServiceMethodInterceptor;
public class CglibProxyFactory {
public static BankServiceImpl createProxy() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(BankServiceImpl.class); // target class (no interface needed)
enhancer.setCallback(new BankServiceMethodInterceptor());
return (BankServiceImpl) enhancer.create(); // generate subclass proxy
}
}
JDK Proxy vs CGLIB Proxy
| Aspect | JDK Dynamic Proxy | CGLIB Proxy |
|---|---|---|
| Mechanism | Implements interfaces at runtime | Creates subclass at runtime |
| Requires interface? | Yes (must proxy an interface) | No (can proxy concrete classes) |
| Library | Built into JDK (java.lang.reflect.Proxy) | External library (cglib.jar + asm.jar) |
| Performance | Slightly slower (reflection-based) | Faster (bytecode generation) |
| Cannot proxy | Concrete classes without interface | Final classes, final methods |
| Used by | Spring AOP (interface-based beans) | Spring AOP (class-based beans), Mockito, Hibernate |
7. Connection to Spring AOP
Spring AOP (Aspect-Oriented Programming) is built entirely on the Proxy pattern. When you add @Transactional, @Cacheable, @Secured, or custom aspects, Spring creates a proxy around your bean. The proxy intercepts method calls and adds the cross-cutting concern before/after the real method.
- If the bean implements an interface: Spring uses JDK Dynamic Proxy
- If the bean is a concrete class: Spring uses CGLIB Proxy
Mocking frameworks like Mockito also use CGLIB/ByteBuddy to generate proxies for the classes being mocked.
// Spring @Transactional creates a proxy around the service
@Service
public class TransferService {
@Transactional // Spring creates a proxy that wraps this method
public void transfer(String from, String to, BigDecimal amount) {
// Spring proxy: BEGIN TRANSACTION (before)
accountRepository.debit(from, amount);
accountRepository.credit(to, amount);
// Spring proxy: COMMIT TRANSACTION (after)
// Spring proxy: ROLLBACK on exception
}
}
// What Spring does internally:
// TransferService proxy = CglibProxyFactory.createProxy(TransferService.class);
// proxy adds: begin transaction -> call real method -> commit/rollback
8. Full-Length Industry Project: API Gateway with Proxy
National Bank of India's microservices architecture needs an API Gateway that sits in front of backend services. The gateway acts as a proxy that adds: authentication (protection proxy), request logging (logging proxy), response caching (virtual proxy), and rate limiting. Backend services contain only business logic; the gateway handles all cross-cutting concerns.
High-Level Design (HLD)
Low-Level Design (LLD)
Project Structure
BankingService.java (interface)
package com.nbi.gateway.service;
import com.nbi.gateway.model.AccountResponse;
import com.nbi.gateway.model.TransferResponse;
import java.math.BigDecimal;
import java.util.List;
public interface BankingService {
AccountResponse getAccountBalance(String accountNo);
TransferResponse transfer(String from, String to, BigDecimal amount);
List<String> getTransactionHistory(String accountNo);
}
Model classes
package com.nbi.gateway.model;
import java.math.BigDecimal;
public class AccountResponse {
private final String accountNo;
private final BigDecimal balance;
private final String holderName;
public AccountResponse(String accountNo, BigDecimal balance, String holderName) {
this.accountNo = accountNo;
this.balance = balance;
this.holderName = holderName;
}
public String getAccountNo() { return accountNo; }
public BigDecimal getBalance() { return balance; }
@Override
public String toString() {
return "Account: " + accountNo + " | Holder: " + holderName
+ " | Balance: Rs " + balance;
}
}
public class TransferResponse {
private final boolean success;
private final String transactionId;
private final String message;
public TransferResponse(boolean success, String transactionId, String message) {
this.success = success;
this.transactionId = transactionId;
this.message = message;
}
public boolean isSuccess() { return success; }
@Override
public String toString() {
return (success ? "SUCCESS" : "FAILED") + " | " + transactionId + " | " + message;
}
}
RealBankingService.java
package com.nbi.gateway.service;
import com.nbi.gateway.model.AccountResponse;
import com.nbi.gateway.model.TransferResponse;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class RealBankingService implements BankingService {
public RealBankingService() {
System.out.println("[REAL SERVICE] BankingService initialized");
}
@Override
public AccountResponse getAccountBalance(String accountNo) {
System.out.println("[REAL SERVICE] Querying database for " + accountNo);
// Simulate DB query
return new AccountResponse(accountNo, new BigDecimal("250000.00"), "Rajesh Kumar");
}
@Override
public TransferResponse transfer(String from, String to, BigDecimal amount) {
System.out.println("[REAL SERVICE] Processing transfer: " + from + " -> " + to);
String txnId = "TXN-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
return new TransferResponse(true, txnId,
"Transferred Rs " + amount + " from " + from + " to " + to);
}
@Override
public List<String> getTransactionHistory(String accountNo) {
System.out.println("[REAL SERVICE] Fetching history for " + accountNo);
return Arrays.asList(
"2026-11-01 | CREDIT | Rs 50,000 | Salary",
"2026-11-01 | DEBIT | Rs 5,000 | UPI Transfer",
"2026-10-30 | DEBIT | Rs 2,500 | Bill Payment"
);
}
}
BankingServiceProxy.java (Proxy with all concerns)
package com.nbi.gateway.proxy;
import com.nbi.gateway.model.AccountResponse;
import com.nbi.gateway.model.TransferResponse;
import com.nbi.gateway.service.BankingService;
import com.nbi.gateway.service.RealBankingService;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public class BankingServiceProxy implements BankingService {
private BankingService realService; // lazy init (Virtual Proxy)
private final String authToken;
private final Map<String, Object> cache = new HashMap<>();
private final AtomicInteger requestCount = new AtomicInteger(0);
private static final int MAX_REQUESTS_PER_MINUTE = 10;
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
public BankingServiceProxy(String authToken) {
this.authToken = authToken;
}
@Override
public AccountResponse getAccountBalance(String accountNo) {
String method = "getAccountBalance";
// 1. Authentication (Protection Proxy)
authenticate(method);
// 2. Rate limiting (Protection Proxy)
checkRateLimit(method);
// 3. Check cache (Virtual Proxy / Caching Proxy)
String cacheKey = "balance_" + accountNo;
if (cache.containsKey(cacheKey)) {
log(method, "CACHE HIT for " + accountNo);
return (AccountResponse) cache.get(cacheKey);
}
// 4. Log request
log(method, "Request for " + accountNo);
// 5. Lazy init + delegate to real service
AccountResponse response = getRealService().getAccountBalance(accountNo);
// 6. Cache response
cache.put(cacheKey, response);
log(method, "Response cached for " + accountNo);
// 7. Log response
log(method, "Response: " + response);
return response;
}
@Override
public TransferResponse transfer(String from, String to, BigDecimal amount) {
String method = "transfer";
authenticate(method);
checkRateLimit(method);
log(method, "Transfer request: " + from + " -> " + to + " Rs " + amount);
// Transfers are never cached
TransferResponse response = getRealService().transfer(from, to, amount);
// Invalidate balance cache for both accounts
cache.remove("balance_" + from);
cache.remove("balance_" + to);
log(method, "Balance cache invalidated for " + from + " and " + to);
log(method, "Response: " + response);
return response;
}
@Override
public List<String> getTransactionHistory(String accountNo) {
String method = "getTransactionHistory";
authenticate(method);
checkRateLimit(method);
log(method, "History request for " + accountNo);
List<String> history = getRealService().getTransactionHistory(accountNo);
log(method, "Returned " + history.size() + " transactions");
return history;
}
// Protection Proxy: authenticate
private void authenticate(String method) {
if (authToken == null || authToken.isEmpty()) {
log(method, "BLOCKED - No auth token");
throw new SecurityException("Authentication required. No token provided.");
}
if (!authToken.startsWith("Bearer ")) {
log(method, "BLOCKED - Invalid token format");
throw new SecurityException("Invalid token format. Expected: Bearer ");
}
log(method, "Authenticated with token: " + authToken.substring(0, 15) + "...");
}
// Protection Proxy: rate limiting
private void checkRateLimit(String method) {
int count = requestCount.incrementAndGet();
if (count > MAX_REQUESTS_PER_MINUTE) {
log(method, "RATE LIMITED - " + count + "/" + MAX_REQUESTS_PER_MINUTE);
throw new RuntimeException("Rate limit exceeded. Max " + MAX_REQUESTS_PER_MINUTE
+ " requests per minute. Try again later.");
}
log(method, "Rate: " + count + "/" + MAX_REQUESTS_PER_MINUTE);
}
// Virtual Proxy: lazy initialization of real service
private BankingService getRealService() {
if (realService == null) {
realService = new RealBankingService();
}
return realService;
}
private void log(String method, String message) {
String timestamp = LocalDateTime.now().format(FORMATTER);
System.out.println("[" + timestamp + "] [PROXY:" + method + "] " + message);
}
}
Application.java
package com.nbi.gateway;
import com.nbi.gateway.model.AccountResponse;
import com.nbi.gateway.model.TransferResponse;
import com.nbi.gateway.proxy.BankingServiceProxy;
import com.nbi.gateway.service.BankingService;
import java.math.BigDecimal;
import java.util.List;
public class Application {
public static void main(String[] args) {
// Scenario 1: Authenticated user checks balance
System.out.println("=== Scenario 1: Check Balance ===");
BankingService service = new BankingServiceProxy("Bearer jwt_token_abc123");
AccountResponse balance = service.getAccountBalance("SAV2001");
System.out.println("Result: " + balance);
System.out.println();
// Scenario 2: Second call hits cache
System.out.println("=== Scenario 2: Cache Hit ===");
AccountResponse cached = service.getAccountBalance("SAV2001");
System.out.println("Result: " + cached);
System.out.println();
// Scenario 3: Transfer (invalidates cache)
System.out.println("=== Scenario 3: Transfer ===");
TransferResponse transfer = service.transfer("SAV2001", "CUR3001",
new BigDecimal("50000"));
System.out.println("Result: " + transfer);
System.out.println();
// Scenario 4: Balance after transfer (cache was invalidated, fetches fresh)
System.out.println("=== Scenario 4: Fresh Balance After Transfer ===");
AccountResponse freshBalance = service.getAccountBalance("SAV2001");
System.out.println("Result: " + freshBalance);
System.out.println();
// Scenario 5: Transaction history
System.out.println("=== Scenario 5: Transaction History ===");
List<String> history = service.getTransactionHistory("SAV2001");
history.forEach(t -> System.out.println(" " + t));
System.out.println();
// Scenario 6: No authentication (blocked)
System.out.println("=== Scenario 6: No Auth Token ===");
try {
BankingService noAuth = new BankingServiceProxy("");
noAuth.getAccountBalance("SAV2001");
} catch (SecurityException e) {
System.out.println("BLOCKED: " + e.getMessage());
}
System.out.println();
// Scenario 7: Rate limiting
System.out.println("=== Scenario 7: Rate Limiting ===");
BankingService rateLimitTest = new BankingServiceProxy("Bearer test_token");
for (int i = 1; i <= 12; i++) {
try {
rateLimitTest.getAccountBalance("SAV2001");
} catch (RuntimeException e) {
System.out.println("Request " + i + ": " + e.getMessage());
}
}
}
}
Expected Output
Key observations from the project
- Four proxy concerns in one class: Authentication (protection), rate limiting (protection), caching (virtual), logging (logging proxy). The real service has zero knowledge of these concerns.
- Lazy initialization: The
RealBankingServiceis created only on the first actual method call (scenario 1). Before that, only the proxy exists in memory. - Cache with invalidation:
getAccountBalancecaches the response.transferinvalidates the cache for affected accounts. The next balance call fetches fresh data. - Rate limiting with AtomicInteger: Thread-safe counter. After 10 requests, further calls are blocked with a clear error message.
- Same interface:
BankingServiceProxyimplementsBankingService. The client works with the interface and does not know it is talking to a proxy. - Seven scenarios cover: Normal flow, cache hit, transfer with cache invalidation, fresh fetch, history, auth failure, rate limiting.
9. When to Use and When NOT to Use
When to use Proxy
| Scenario | Proxy Type | Example |
|---|---|---|
| Access control / security | Protection Proxy | Check permissions before allowing method call |
| Lazy loading of expensive objects | Virtual Proxy | Load image/data only when displayed |
| Caching responses | Caching Proxy | Return cached result for repeated queries |
| Logging and auditing | Logging Proxy | Log every method call with timestamp |
| Rate limiting | Protection Proxy | Limit API calls per client per minute |
| Remote object access | Remote Proxy | RMI stub, gRPC client, REST client wrapper |
| Spring AOP | Dynamic Proxy | @Transactional, @Cacheable, @Secured |
When NOT to use Proxy
| Scenario | Why NOT | Use Instead |
|---|---|---|
| No cross-cutting concerns | Proxy adds indirection for no benefit | Direct service call |
| Simple decorator needed | If only adding behaviour, Decorator is simpler | Decorator pattern |
| Performance-critical path | Proxy adds method call overhead | Direct call or inline logic |
Proxy vs Decorator vs Adapter
| Aspect | Proxy | Decorator | Adapter |
|---|---|---|---|
| Purpose | Control access to the object | Add responsibilities to the object | Convert interface to another interface |
| Same interface? | Yes (same as real object) | Yes (same as wrapped object) | No (converts to target interface) |
| Knows real object? | Yes (creates or holds reference) | No (wraps any component) | Yes (wraps the adaptee) |
| Stackable? | Usually single proxy | Multiple decorators stackable | Usually single adapter |
10. Quick Reference Card
| Component | Role | In API Gateway Project |
|---|---|---|
| Subject Interface | Common interface for real and proxy | BankingService |
| Real Subject | The actual implementation with business logic | RealBankingService |
| Proxy | Controls access, adds concerns, delegates to real | BankingServiceProxy |
| Client | Uses the interface, unaware of proxy | Application |