Hardstructural18 min read

Proxy Design Pattern

1. What is Proxy Pattern?

Definition

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

TypePurposeExample
Protection ProxyControls access based on permissions/authorization. Adds security layer before the real object.Bank service proxy checks if user is authorized before allowing withdrawal
Virtual ProxyLazy loading. Creates the expensive real object only when actually needed.Image proxy loads the actual image only when display() is called
Remote ProxyRepresents an object in a different address space (different JVM, different server).RMI stub acts as local proxy for a remote object

2. The Problem

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

Scenario

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());
        }
    }
}
=== Scenario 1: User checks balance === BankServiceImpl created (real service) [PROXY] Rajesh accessing balance Account: SAV2001 | Balance: Rs 50000.0 === Scenario 2: Admin adds balance === BankServiceImpl created (real service) [PROXY] ADMIN Priya adding balance Deposited Rs 25000.0 to SAV2001. New balance: Rs 75000.0 === Scenario 3: User tries admin operation === BLOCKED: Access denied: only ADMIN can add balance. Your role: USER

4. Two Ways to Generate Proxy Classes

ApproachDescriptionWhen to use
a) Manual ProxyWrite 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

Normal Java class: .java (HDD) --javac--> .class (HDD) --JVM--> loads and executes (in JVM Memory of the RAM) InMemory class / Dynamic class: App execution ---> InMemory class generation ---> Source code compilation ---> Byte code execution (JVM Memory (source code generation and byte code generation (JVM Memory of RAM) of RAM) in JVM memory of RAM) (JVM memory of RAM)

5. JDK Dynamic Proxy (InvocationHandler)

JDK Proxy

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));
BankServiceImpl created (real service) [JDK PROXY] Rajesh calling getBalance [JDK PROXY] getBalance completed Account: SAV2001 | Balance: Rs 50000.0

6. CGLIB Dynamic Proxy (MethodInterceptor)

CGLIB Proxy

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

AspectJDK Dynamic ProxyCGLIB Proxy
MechanismImplements interfaces at runtimeCreates subclass at runtime
Requires interface?Yes (must proxy an interface)No (can proxy concrete classes)
LibraryBuilt into JDK (java.lang.reflect.Proxy)External library (cglib.jar + asm.jar)
PerformanceSlightly slower (reflection-based)Faster (bytecode generation)
Cannot proxyConcrete classes without interfaceFinal classes, final methods
Used bySpring AOP (interface-based beans)Spring AOP (class-based beans), Mockito, Hibernate

7. Connection to Spring AOP

Spring AOP and Proxy

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

Business Scenario

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)

+---------------------------------------------------+ | CLIENT LAYER | | Mobile App / Browser | | | | | v | +---------------------------------------------------+ | API GATEWAY (Proxy Layer) | | | | ServiceProxy (implements same interface as real) | | 1. AuthenticationFilter (protection proxy) | | 2. RateLimiter (protection proxy) | | 3. RequestLogger (logging proxy) | | 4. ResponseCache (virtual proxy / caching) | | 5. Delegates to real service | | 6. ResponseLogger (after proxy) | +---------------------------------------------------+ | v +---------------------------------------------------+ | BACKEND SERVICES (Real Objects) | | AccountService TransferService LoanService | | (pure business logic, no security/logging) | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | BankingService (interface) | +-------------------------------------------------------+ | + getAccountBalance(acno): AccountResponse | | + transfer(from, to, amount): TransferResponse | | + getTransactionHistory(acno): List<Transaction> | +-------------------------------------------------------+ ^ ^ | | +-------------------+ +----------------------------+ | RealBankingService| | BankingServiceProxy | | (actual logic) | | (implements BankingService) | +-------------------+ +----------------------------+ | - realService: BankingService| | - authToken: String | | - cache: Map | | - requestCount: AtomicInteger | | - MAX_REQUESTS: int | +----------------------------+ | 1. authenticate() | | 2. checkRateLimit() | | 3. checkCache() | | 4. logRequest() | | 5. delegate to realService | | 6. cacheResponse() | | 7. logResponse() | +----------------------------+

Project Structure

nbi-api-gateway/ ├── src/ │ └── main/java/com/nbi/gateway/ │ ├── model/ │ │ ├── AccountResponse.java │ │ ├── TransferResponse.java │ │ └── Transaction.java │ ├── service/ │ │ ├── BankingService.java (interface) │ │ └── RealBankingService.java (real implementation) │ ├── proxy/ │ │ └── BankingServiceProxy.java (proxy with all concerns) │ └── Application.java ├── src/ │ └── test/java/com/nbi/gateway/ │ └── proxy/ │ └── BankingServiceProxyTest.java └── pom.xml

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

=== Scenario 1: Check Balance === [18:00:00.001] [PROXY:getAccountBalance] Authenticated with token: Bearer jwt_tok... [18:00:00.001] [PROXY:getAccountBalance] Rate: 1/10 [18:00:00.002] [PROXY:getAccountBalance] Request for SAV2001 [REAL SERVICE] BankingService initialized [REAL SERVICE] Querying database for SAV2001 [18:00:00.003] [PROXY:getAccountBalance] Response cached for SAV2001 [18:00:00.003] [PROXY:getAccountBalance] Response: Account: SAV2001 | Holder: Rajesh Kumar | Balance: Rs 250000.00 Result: Account: SAV2001 | Holder: Rajesh Kumar | Balance: Rs 250000.00 === Scenario 2: Cache Hit === [18:00:00.004] [PROXY:getAccountBalance] Authenticated with token: Bearer jwt_tok... [18:00:00.004] [PROXY:getAccountBalance] Rate: 2/10 [18:00:00.004] [PROXY:getAccountBalance] CACHE HIT for SAV2001 Result: Account: SAV2001 | Holder: Rajesh Kumar | Balance: Rs 250000.00 === Scenario 3: Transfer === [18:00:00.005] [PROXY:transfer] Authenticated with token: Bearer jwt_tok... [18:00:00.005] [PROXY:transfer] Rate: 3/10 [18:00:00.005] [PROXY:transfer] Transfer request: SAV2001 -> CUR3001 Rs 50000 [REAL SERVICE] Processing transfer: SAV2001 -> CUR3001 [18:00:00.006] [PROXY:transfer] Balance cache invalidated for SAV2001 and CUR3001 [18:00:00.006] [PROXY:transfer] Response: SUCCESS | TXN-A1B2C3D4 | Transferred Rs 50000 Result: SUCCESS | TXN-A1B2C3D4 | Transferred Rs 50000 from SAV2001 to CUR3001 === Scenario 4: Fresh Balance After Transfer === [18:00:00.007] [PROXY:getAccountBalance] Authenticated with token: Bearer jwt_tok... [18:00:00.007] [PROXY:getAccountBalance] Rate: 4/10 [18:00:00.007] [PROXY:getAccountBalance] Request for SAV2001 [REAL SERVICE] Querying database for SAV2001 [18:00:00.008] [PROXY:getAccountBalance] Response cached for SAV2001 Result: Account: SAV2001 | Holder: Rajesh Kumar | Balance: Rs 250000.00 === Scenario 5: Transaction History === ... === Scenario 6: No Auth Token === [18:00:00.010] [PROXY:getAccountBalance] BLOCKED - Invalid token format BLOCKED: Invalid token format. Expected: Bearer <token> === Scenario 7: Rate Limiting === ... (requests 1-10 succeed, requests 11-12 blocked) Request 11: Rate limit exceeded. Max 10 requests per minute. Try again later. Request 12: Rate limit exceeded. Max 10 requests per minute. Try again later.

Key observations from the project

  1. 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.
  2. Lazy initialization: The RealBankingService is created only on the first actual method call (scenario 1). Before that, only the proxy exists in memory.
  3. Cache with invalidation: getAccountBalance caches the response. transfer invalidates the cache for affected accounts. The next balance call fetches fresh data.
  4. Rate limiting with AtomicInteger: Thread-safe counter. After 10 requests, further calls are blocked with a clear error message.
  5. Same interface: BankingServiceProxy implements BankingService. The client works with the interface and does not know it is talking to a proxy.
  6. 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

ScenarioProxy TypeExample
Access control / securityProtection ProxyCheck permissions before allowing method call
Lazy loading of expensive objectsVirtual ProxyLoad image/data only when displayed
Caching responsesCaching ProxyReturn cached result for repeated queries
Logging and auditingLogging ProxyLog every method call with timestamp
Rate limitingProtection ProxyLimit API calls per client per minute
Remote object accessRemote ProxyRMI stub, gRPC client, REST client wrapper
Spring AOPDynamic Proxy@Transactional, @Cacheable, @Secured

When NOT to use Proxy

ScenarioWhy NOTUse Instead
No cross-cutting concernsProxy adds indirection for no benefitDirect service call
Simple decorator neededIf only adding behaviour, Decorator is simplerDecorator pattern
Performance-critical pathProxy adds method call overheadDirect call or inline logic

Proxy vs Decorator vs Adapter

AspectProxyDecoratorAdapter
PurposeControl access to the objectAdd responsibilities to the objectConvert 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 proxyMultiple decorators stackableUsually single adapter

10. Quick Reference Card

ComponentRoleIn API Gateway Project
Subject InterfaceCommon interface for real and proxyBankingService
Real SubjectThe actual implementation with business logicRealBankingService
ProxyControls access, adds concerns, delegates to realBankingServiceProxy
ClientUses the interface, unaware of proxyApplication
Structural GOF Pattern Hard

Proxy Design Pattern in Java

Complete Tutorial | 12 Sections | Full API Gateway Project with HLD and LLD | Protection Proxy + Virtual Proxy + Dynamic Proxy | CGLIB and JDK Proxy | Spring AOP Connection | Multiple Scenarios | 12 Interview Questions

1. What is Proxy Pattern?

Definition

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

TypePurposeExample
Protection ProxyControls access based on permissions/authorization. Adds security layer before the real object.Bank service proxy checks if user is authorized before allowing withdrawal
Virtual ProxyLazy loading. Creates the expensive real object only when actually needed.Image proxy loads the actual image only when display() is called
Remote ProxyRepresents an object in a different address space (different JVM, different server).RMI stub acts as local proxy for a remote object

2. The Problem

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

Scenario

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());
        }
    }
}
=== Scenario 1: User checks balance === BankServiceImpl created (real service) [PROXY] Rajesh accessing balance Account: SAV2001 | Balance: Rs 50000.0 === Scenario 2: Admin adds balance === BankServiceImpl created (real service) [PROXY] ADMIN Priya adding balance Deposited Rs 25000.0 to SAV2001. New balance: Rs 75000.0 === Scenario 3: User tries admin operation === BLOCKED: Access denied: only ADMIN can add balance. Your role: USER

4. Two Ways to Generate Proxy Classes

ApproachDescriptionWhen to use
a) Manual ProxyWrite 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

Normal Java class: .java (HDD) --javac--> .class (HDD) --JVM--> loads and executes (in JVM Memory of the RAM) InMemory class / Dynamic class: App execution ---> InMemory class generation ---> Source code compilation ---> Byte code execution (JVM Memory (source code generation and byte code generation (JVM Memory of RAM) of RAM) in JVM memory of RAM) (JVM memory of RAM)

5. JDK Dynamic Proxy (InvocationHandler)

JDK Proxy

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));
BankServiceImpl created (real service) [JDK PROXY] Rajesh calling getBalance [JDK PROXY] getBalance completed Account: SAV2001 | Balance: Rs 50000.0

6. CGLIB Dynamic Proxy (MethodInterceptor)

CGLIB Proxy

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

AspectJDK Dynamic ProxyCGLIB Proxy
MechanismImplements interfaces at runtimeCreates subclass at runtime
Requires interface?Yes (must proxy an interface)No (can proxy concrete classes)
LibraryBuilt into JDK (java.lang.reflect.Proxy)External library (cglib.jar + asm.jar)
PerformanceSlightly slower (reflection-based)Faster (bytecode generation)
Cannot proxyConcrete classes without interfaceFinal classes, final methods
Used bySpring AOP (interface-based beans)Spring AOP (class-based beans), Mockito, Hibernate

7. Connection to Spring AOP

Spring AOP and Proxy

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

Business Scenario

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)

+---------------------------------------------------+ | CLIENT LAYER | | Mobile App / Browser | | | | | v | +---------------------------------------------------+ | API GATEWAY (Proxy Layer) | | | | ServiceProxy (implements same interface as real) | | 1. AuthenticationFilter (protection proxy) | | 2. RateLimiter (protection proxy) | | 3. RequestLogger (logging proxy) | | 4. ResponseCache (virtual proxy / caching) | | 5. Delegates to real service | | 6. ResponseLogger (after proxy) | +---------------------------------------------------+ | v +---------------------------------------------------+ | BACKEND SERVICES (Real Objects) | | AccountService TransferService LoanService | | (pure business logic, no security/logging) | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | BankingService (interface) | +-------------------------------------------------------+ | + getAccountBalance(acno): AccountResponse | | + transfer(from, to, amount): TransferResponse | | + getTransactionHistory(acno): List<Transaction> | +-------------------------------------------------------+ ^ ^ | | +-------------------+ +----------------------------+ | RealBankingService| | BankingServiceProxy | | (actual logic) | | (implements BankingService) | +-------------------+ +----------------------------+ | - realService: BankingService| | - authToken: String | | - cache: Map | | - requestCount: AtomicInteger | | - MAX_REQUESTS: int | +----------------------------+ | 1. authenticate() | | 2. checkRateLimit() | | 3. checkCache() | | 4. logRequest() | | 5. delegate to realService | | 6. cacheResponse() | | 7. logResponse() | +----------------------------+

Project Structure

nbi-api-gateway/ ├── src/ │ └── main/java/com/nbi/gateway/ │ ├── model/ │ │ ├── AccountResponse.java │ │ ├── TransferResponse.java │ │ └── Transaction.java │ ├── service/ │ │ ├── BankingService.java (interface) │ │ └── RealBankingService.java (real implementation) │ ├── proxy/ │ │ └── BankingServiceProxy.java (proxy with all concerns) │ └── Application.java ├── src/ │ └── test/java/com/nbi/gateway/ │ └── proxy/ │ └── BankingServiceProxyTest.java └── pom.xml

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

=== Scenario 1: Check Balance === [18:00:00.001] [PROXY:getAccountBalance] Authenticated with token: Bearer jwt_tok... [18:00:00.001] [PROXY:getAccountBalance] Rate: 1/10 [18:00:00.002] [PROXY:getAccountBalance] Request for SAV2001 [REAL SERVICE] BankingService initialized [REAL SERVICE] Querying database for SAV2001 [18:00:00.003] [PROXY:getAccountBalance] Response cached for SAV2001 [18:00:00.003] [PROXY:getAccountBalance] Response: Account: SAV2001 | Holder: Rajesh Kumar | Balance: Rs 250000.00 Result: Account: SAV2001 | Holder: Rajesh Kumar | Balance: Rs 250000.00 === Scenario 2: Cache Hit === [18:00:00.004] [PROXY:getAccountBalance] Authenticated with token: Bearer jwt_tok... [18:00:00.004] [PROXY:getAccountBalance] Rate: 2/10 [18:00:00.004] [PROXY:getAccountBalance] CACHE HIT for SAV2001 Result: Account: SAV2001 | Holder: Rajesh Kumar | Balance: Rs 250000.00 === Scenario 3: Transfer === [18:00:00.005] [PROXY:transfer] Authenticated with token: Bearer jwt_tok... [18:00:00.005] [PROXY:transfer] Rate: 3/10 [18:00:00.005] [PROXY:transfer] Transfer request: SAV2001 -> CUR3001 Rs 50000 [REAL SERVICE] Processing transfer: SAV2001 -> CUR3001 [18:00:00.006] [PROXY:transfer] Balance cache invalidated for SAV2001 and CUR3001 [18:00:00.006] [PROXY:transfer] Response: SUCCESS | TXN-A1B2C3D4 | Transferred Rs 50000 Result: SUCCESS | TXN-A1B2C3D4 | Transferred Rs 50000 from SAV2001 to CUR3001 === Scenario 4: Fresh Balance After Transfer === [18:00:00.007] [PROXY:getAccountBalance] Authenticated with token: Bearer jwt_tok... [18:00:00.007] [PROXY:getAccountBalance] Rate: 4/10 [18:00:00.007] [PROXY:getAccountBalance] Request for SAV2001 [REAL SERVICE] Querying database for SAV2001 [18:00:00.008] [PROXY:getAccountBalance] Response cached for SAV2001 Result: Account: SAV2001 | Holder: Rajesh Kumar | Balance: Rs 250000.00 === Scenario 5: Transaction History === ... === Scenario 6: No Auth Token === [18:00:00.010] [PROXY:getAccountBalance] BLOCKED - Invalid token format BLOCKED: Invalid token format. Expected: Bearer <token> === Scenario 7: Rate Limiting === ... (requests 1-10 succeed, requests 11-12 blocked) Request 11: Rate limit exceeded. Max 10 requests per minute. Try again later. Request 12: Rate limit exceeded. Max 10 requests per minute. Try again later.

Key observations from the project

  1. 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.
  2. Lazy initialization: The RealBankingService is created only on the first actual method call (scenario 1). Before that, only the proxy exists in memory.
  3. Cache with invalidation: getAccountBalance caches the response. transfer invalidates the cache for affected accounts. The next balance call fetches fresh data.
  4. Rate limiting with AtomicInteger: Thread-safe counter. After 10 requests, further calls are blocked with a clear error message.
  5. Same interface: BankingServiceProxy implements BankingService. The client works with the interface and does not know it is talking to a proxy.
  6. 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

ScenarioProxy TypeExample
Access control / securityProtection ProxyCheck permissions before allowing method call
Lazy loading of expensive objectsVirtual ProxyLoad image/data only when displayed
Caching responsesCaching ProxyReturn cached result for repeated queries
Logging and auditingLogging ProxyLog every method call with timestamp
Rate limitingProtection ProxyLimit API calls per client per minute
Remote object accessRemote ProxyRMI stub, gRPC client, REST client wrapper
Spring AOPDynamic Proxy@Transactional, @Cacheable, @Secured

When NOT to use Proxy

ScenarioWhy NOTUse Instead
No cross-cutting concernsProxy adds indirection for no benefitDirect service call
Simple decorator neededIf only adding behaviour, Decorator is simplerDecorator pattern
Performance-critical pathProxy adds method call overheadDirect call or inline logic

Proxy vs Decorator vs Adapter

AspectProxyDecoratorAdapter
PurposeControl access to the objectAdd responsibilities to the objectConvert 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 proxyMultiple decorators stackableUsually single adapter

10. Quick Reference Card

ComponentRoleIn API Gateway Project
Subject InterfaceCommon interface for real and proxyBankingService
Real SubjectThe actual implementation with business logicRealBankingService
ProxyControls access, adds concerns, delegates to realBankingServiceProxy
ClientUses the interface, unaware of proxyApplication