Structural

Proxy

Provide a surrogate or placeholder for another object to control access to it.

Intermediate Pattern #12 of 23

What is the Proxy Pattern?

Proxy provides a substitute for another object and controls access to it. The proxy implements the same interface as the real subject, so clients can't tell the difference. The proxy can do work before or after forwarding the request — lazy loading, caching, access control, logging, and more.

Four major proxy flavours exist: Virtual (lazy init), Protection (access control), Remote (network transparency), and Caching (result memoisation).

Real-world analogy: A credit card is a proxy for your bank account. It implements the same "make payment" interface, but adds security checks, spending limits, and logging before forwarding to the actual account.

The Problem It Solves

You have a heavyweight service object that loads a large image from disk. Most of the time it isn't even used — but it's always initialised at startup, wasting memory and time.

A Virtual Proxy can stand in for the real object, delaying its creation until the first actual use. The client never changes — it just calls methods on what it thinks is the real object, but it's actually calling the lightweight proxy.

Participants

Subject (interface)
Common interface for RealSubject and Proxy. Allows a Proxy to be used anywhere a RealSubject is expected.
RealSubject
The actual object that does the real work. The proxy delegates to this eventually.
Proxy
Maintains a reference to RealSubject. Implements Subject. Intercepts calls — adds lazy loading, caching, logging, or access checks before/after delegating to RealSubject.
Client
Works through the Subject interface — completely unaware it's talking to a Proxy rather than the RealSubject.

Visual Flow Diagram

«interface» Subject request() Client uses Subject Proxy - realSubject: RealSubject request() { checkAccess() realSubject.request() logAccess() RealSubject (heavyweight/remote) request() — real work expensive to create delegates Virtual lazy init Protection access ctrl Remote network Caching memoisation Logging audit trail

Java Code Example

Java — Virtual + Protection + Caching Proxies
// Subject interface
public interface Image {
    void display();
}

// RealSubject — expensive to create
public class RealImage implements Image {
    private final String filename;

    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk(); // expensive — disk I/O
    }

    private void loadFromDisk() {
        System.out.println("Loading " + filename + " from disk...");
    }

    public void display() {
        System.out.println("Displaying " + filename);
    }
}

// ── VIRTUAL PROXY — lazy initialisation ─────────────────────────────
public class LazyImageProxy implements Image {
    private RealImage realImage; // null until first use
    private final String filename;

    public LazyImageProxy(String filename) {
        this.filename = filename; // cheap — no disk load yet
    }

    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename); // load only on first call
        }
        realImage.display(); // delegate
    }
}

// ── PROTECTION PROXY — access control ───────────────────────────────
public class ProtectedImageProxy implements Image {
    private final RealImage realImage;
    private final String    userRole;

    public ProtectedImageProxy(String filename, String role) {
        this.realImage = new RealImage(filename);
        this.userRole  = role;
    }

    public void display() {
        if (!userRole.equals("ADMIN") && !userRole.equals("USER")) {
            throw new SecurityException("Access denied for role: " + userRole);
        }
        System.out.println("[ACCESS LOG] user=" + userRole);
        realImage.display();
    }
}

// ── CACHING PROXY — memoisation ──────────────────────────────────────
public interface DataService {
    String fetchData(String query);
}

public class CachingDataProxy implements DataService {
    private final DataService            realService;
    private final Map<String, String>  cache = new HashMap<>();

    public CachingDataProxy(DataService real) { this.realService = real; }

    public String fetchData(String query) {
        if (cache.containsKey(query)) {
            System.out.println("[CACHE HIT] " + query);
            return cache.get(query);
        }
        String result = realService.fetchData(query); // expensive DB call
        cache.put(query, result);
        return result;
    }
}

// ── Dynamic Proxy (Java built-in) ────────────────────────────────────
public class LoggingHandler implements InvocationHandler {
    private final Object target;
    public LoggingHandler(Object target) { this.target = target; }

    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        System.out.println("[LOG] Calling: " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("[LOG] Done: " + method.getName());
        return result;
    }
}

// Client creates a JDK dynamic proxy
Image proxy = (Image) Proxy.newProxyInstance(
    Image.class.getClassLoader(),
    new Class[]{ Image.class },
    new LoggingHandler(new RealImage("photo.jpg"))
);
proxy.display(); // logs before and after

When to Use / Avoid

✓ Use When

  • Virtual: Object initialisation is expensive and may not be needed
  • Protection: You need access control without changing the real subject
  • Remote: You want local-transparent access to a remote object
  • Caching: Results of expensive operations can be memoised
  • Logging/Audit: Cross-cutting concerns without modifying the real class

✕ Avoid When

  • The added indirection degrades response time in latency-critical paths
  • The real subject is simple — proxy adds unjustified complexity
  • You can add the cross-cutting concern directly to the subject (small codebase)

Real-World Examples

Pros & Cons

Pros

  • Controls access to real object without client knowing
  • Open/Closed — add caching/logging without changing subject
  • Virtual proxy improves startup performance via lazy loading
  • Cross-cutting concerns (security, logging) cleanly separated

Cons

  • Response time may increase due to indirection layer
  • Code complexity increases — extra class per proxied service
  • Stale cache in Caching Proxy can return outdated data
  • Dynamic proxies only work with interfaces in JDK (CGLIB needed for classes)

How Proxy Can Be Broken

⚠ Attack Vectors

  • Bypassing the proxy: Client holds a direct reference to the RealSubject — all access control, caching and logging in the proxy is skipped entirely
  • Stale cache in Caching Proxy: Underlying data changes but cached result is returned — clients receive outdated data with no indication it's stale
  • Race condition in Virtual Proxy: Two threads both see realSubject == null simultaneously and both create a new RealSubject — expensive double initialisation
  • Proxy not covering all interface methods: Proxy only intercepts some methods; client calls an unproxied method directly on the subject through a leaked reference — bypasses all guards
  • Self-invocation bypass in Spring AOP: A method inside the real bean calls another method on this — the call goes directly to the real object, skipping the Spring proxy and its @Transactional/@Cacheable interceptors

✓ Prevention

  • Never expose RealSubject directly: Only inject the Proxy through DI. Make RealSubject package-private or constructor-private from outside its module
  • Cache invalidation strategy: Implement TTL-based expiry, event-driven invalidation, or version tokens in the Caching Proxy — never cache indefinitely without a strategy
  • Thread-safe Virtual Proxy: Use synchronized with double-checked locking or an AtomicReference with compareAndSet for safe lazy initialisation
  • Implement all interface methods in the proxy: The proxy must intercept every method — use a dynamic proxy (java.lang.reflect.Proxy) if the interface is large, to avoid gaps
  • Spring self-invocation fix: Inject the proxy into the bean itself (@Autowired private MyService self) and call self.method(), or use AopContext.currentProxy()
Java — Break & Fix
// ❌ BREAKING — race condition in Virtual Proxy
public void display() {
    if (realImage == null) {              // Thread A and B both see null
        realImage = new RealImage(file); // both create — double init!
    }
    realImage.display();
}

// ✅ FIX — double-checked locking with volatile
private volatile RealImage realImage;

public void display() {
    if (realImage == null) {
        synchronized (this) {
            if (realImage == null) {          // second check inside lock
                realImage = new RealImage(filename);
            }
        }
    }
    realImage.display();
}

// ❌ BREAKING — stale cache, no invalidation
public String fetchData(String q) {
    if (cache.containsKey(q)) return cache.get(q); // never expires!
    String r = real.fetchData(q); cache.put(q, r); return r;
}

// ✅ FIX — TTL-based cache entry
record CacheEntry(String value, long expiresAt) {}
private final Map<String, CacheEntry> cache = new HashMap<>();
private static final long TTL_MS = 60_000;

public String fetchData(String q) {
    CacheEntry entry = cache.get(q);
    if (entry != null && System.currentTimeMillis() < entry.expiresAt()) {
        return entry.value(); // ✅ only serve if not expired
    }
    String r = real.fetchData(q);
    cache.put(q, new CacheEntry(r, System.currentTimeMillis() + TTL_MS));
    return r;
}

// ❌ BREAKING — Spring self-invocation bypasses proxy
public class OrderService {
    public void placeOrder() {
        processPayment(); // ❌ calls this.processPayment() — bypasses @Transactional proxy!
    }
    @Transactional
    public void processPayment() { /* transaction never starts */ }
}

// ✅ FIX — call through the proxy
public class OrderService {
    @Autowired private OrderService self; // inject own proxy

    public void placeOrder() {
        self.processPayment(); // ✅ goes through Spring proxy — @Transactional fires
    }
    @Transactional
    public void processPayment() { /* transaction starts correctly */ }
}

How Other Patterns Relate

Interview Cheat Sheet

  1. What: A stand-in for the real object with the same interface. Intercepts calls to add lazy loading (Virtual), access control (Protection), caching (Caching), or network transparency (Remote) — client never knows.
  2. 4 types to know: Virtual (lazy init), Protection (security check), Remote (local stub for remote object), Caching (memoises expensive results). Spring AOP uses dynamic proxies for all cross-cutting concerns.
  3. Critical Java gotcha: Spring AOP self-invocation bug — this.method() inside a bean bypasses the proxy. Fix by injecting self or using AopContext.currentProxy(). Comes up in every Spring interview.