Proxy Design Pattern in Java: Complete Guide with Examples (2026)

Proxy Design Pattern in Java: Complete Guide with Examples (2026)

Proxy Design Pattern in Java: Complete Guide with Examples

Learn the Proxy design pattern in Java with simple explanations and practical examples. This guide covers how to control access, add lazy loading, and cache results behind a stand-in object, with before-and-after code for beginners and experts.

I've been writing Java for years, and Proxy is the pattern I reach for whenever I need to add a checkpoint in front of an expensive or sensitive object. Not because the object is wrong—because every client shouldn't have to repeat the same guard logic. Here's how Proxy saved me from duplicating access checks in thirty places.

Why the Proxy Pattern Still Matters in Java

You've built a service. It works. Then requirements arrive: check permissions before every call, cache the response for repeated calls, log every invocation, and lazy-load the heavy object only when someone actually uses it. You start adding these checks into the service itself — and into every client. Now the service has six responsibilities, and the guards are duplicated in every caller.

The Proxy pattern solves this by putting a stand-in in front of the real object. The stand-in has the same interface, so clients don't notice. But the stand-in can check permissions, hit the cache, log, or delay creating the real object until it's actually needed.

Whether you're building video streaming, authentication layers, or lazy-loaded databases, Proxy keeps your real object clean and your cross-cutting concerns in one place.

What is the Proxy Design Pattern?

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

In plain words: Proxy stands in front of the real object and controls access to it. Both implement the same interface, so the client doesn't know the difference. But the proxy can add logic before or after delegating to the real object.

The Players

Role Responsibility Example
Subject Common interface for real object and proxy VideoService
Real Subject The actual object that does the work RealVideoService
Proxy Controls access to the real subject VideoServiceProxy

The Four Common Types of Proxy

Type What It Does Example
Virtual Proxy Delays creation of expensive objects until needed Lazy-load a 4K video
Protection Proxy Checks permissions before allowing calls Only admin can delete a user
Caching Proxy Caches results to avoid repeated expensive calls Cache the exchange rate for 60 seconds
Remote Proxy Represents an object in a different address space RMI, gRPC stub
Proxy Design Pattern Structure Diagram Client calls VideoService interface. VideoServiceProxy implements the same interface and delegates to RealVideoService, adding access control, caching, and logging. PROXY PATTERN STRUCTURE Client (Video Player) playVideo() «interface» VideoService + playVideo(String) VideoServiceProxy (Proxy) - checkPermission() - cache, logging, lazy load RealVideoService (Real Subject) - loads heavy video file - streams to client delegates to Client Subject (interface) Proxy Real Subject

Proxy pattern structure: Client calls VideoService — served by the Proxy, which delegates to RealVideoService after adding access control and caching.

How to read this diagram (for freshers): The blue box at the top is the Client — the video player. It only knows about the VideoService interface (the purple box). Behind that interface sit two implementations: the Proxy (green) on the left, and the Real Subject (yellow) on the right. When the client calls playVideo(), the proxy intercepts it first — checks permission, checks the cache, logs the call — and only then delegates to the real video service. The client never knows the difference. The real service stays clean.

Real-World Example for Freshers

Before we touch code, let's look at the Proxy pattern you already use every day.

Example 1: Video Streaming (Netflix, YouTube)

When you open Netflix and scroll through thumbnails, nothing streams yet. Only when you click "Play" does the video actually start loading. Netflix shows you a preview image instantly — the real 4K video loads only on demand.

Behind that experience is a virtual proxy. The player hands you a proxy object that looks like the video. When you click play, the proxy loads the real video and streams it. Until then, nothing heavy is loaded.

That's the Proxy pattern. The client thinks it's talking to the video. It's actually talking to a placeholder that controls when the real video gets created.

Example 2: ATM Withdrawal

When you withdraw money from an ATM, you don't reach directly into the bank's vault. The ATM is a proxy. It checks your PIN, verifies your balance, checks the daily limit, and only then dispenses cash. The vault — the real subject — never sees a request that violates the rules.

Example 3: Spring Security (@PreAuthorize)

Every time you write @PreAuthorize("hasRole('ADMIN')") on a method, Spring wraps that method in a proxy. The proxy checks the user's role before calling your actual method. Your business logic stays clean — no permission checks anywhere inside.

Example 4: Hibernate Lazy Loading

When you load an Order entity, Hibernate doesn't immediately fetch its Customer. It gives you a proxy for the customer. Only when you call order.getCustomer().getName() does Hibernate hit the database.

Quick Summary for Freshers

Real-World Situation Proxy Real Subject
Video streaming VideoServiceProxy (lazy) RealVideoService
ATM withdrawal ATM (checks PIN) Bank vault
Spring Security AOP Proxy Your @Service method
Hibernate LazyProxy Actual Customer entity

Once you see it in your ATM, you'll see it everywhere. That's why Proxy is one of the most practical design patterns in everyday software.

Violation: The Repeated Guard Nightmare

Imagine you're building the video player. Without a proxy, every client must check permissions and cache the result before calling the real service:

public class VideoPlayer {
    private VideoService service = new RealVideoService();
    private Map<String, String> cache = new HashMap<>();

    public void playVideo(String userId, String videoId) {
        // Every client repeats this
        if (!isPremiumUser(userId)) {
            throw new SecurityException("Only premium users can play 4K videos");
        }
        if (cache.containsKey(videoId)) {
            System.out.println("Playing from cache: " + cache.get(videoId));
            return;
        }
        String result = service.playVideo(videoId);
        cache.put(videoId, result);
        System.out.println(result);
    }
}

The Problem: If five different classes need to play videos, all five repeat the permission check, the cache lookup, and the logging. If the rules change, every caller must be updated. And the real video service loads its 500 MB video file the moment it's constructed, even if nobody ever presses play.

Solution: Proxy with Video Streaming

Let's build a proxy that sits in front of the real video service. It handles permissions, caching, logging, and lazy loading. The client just calls playVideo() and gets the result.

Step 1: The Subject Interface

public interface VideoService {
    String playVideo(String videoId);
}

Step 2: The Real Subject (Heavy Object)

public class RealVideoService implements VideoService {
    public RealVideoService() {
        // Expensive initialization — loads 500 MB of video data
        System.out.println("Loading 500 MB video library into memory...");
    }

    @Override
    public String playVideo(String videoId) {
        return "Streaming " + videoId + " in 4K";
    }
}

Step 3: The Proxy

import java.util.HashMap;
import java.util.Map;

public class VideoServiceProxy implements VideoService {
    private RealVideoService realService;   // lazy — created on first use
    private final Map<String, String> cache = new HashMap<>();
    private final String currentUserRole;

    public VideoServiceProxy(String currentUserRole) {
        this.currentUserRole = currentUserRole;
    }

    @Override
    public String playVideo(String videoId) {
        // 1. Access control
        if (!"PREMIUM".equals(currentUserRole)) {
            throw new SecurityException("Only PREMIUM users can play videos");
        }

        // 2. Cache hit
        if (cache.containsKey(videoId)) {
            System.out.println("[PROXY] Cache hit for " + videoId);
            return cache.get(videoId);
        }

        // 3. Lazy initialization
        if (realService == null) {
            System.out.println("[PROXY] First request — initializing real service");
            realService = new RealVideoService();
        }

        // 4. Logging
        System.out.println("[PROXY] Logging request for " + videoId);

        // 5. Delegate to real object
        String result = realService.playVideo(videoId);
        cache.put(videoId, result);
        return result;
    }
}

Step 4: Using the Proxy

public class Demo {
    public static void main(String[] args) {
        VideoService premiumUser = new VideoServiceProxy("PREMIUM");

        System.out.println(premiumUser.playVideo("movie-123"));
        System.out.println(premiumUser.playVideo("movie-123"));  // cache hit
        System.out.println(premiumUser.playVideo("movie-456"));

        VideoService freeUser = new VideoServiceProxy("FREE");
        try {
            freeUser.playVideo("movie-123");
        } catch (SecurityException e) {
            System.out.println("Blocked: " + e.getMessage());
        }
    }
}

Output:

[PROXY] First request — initializing real service
Loading 500 MB video library into memory...
[PROXY] Logging request for movie-123
Streaming movie-123 in 4K
[PROXY] Cache hit for movie-123
Streaming movie-123 in 4K
[PROXY] Logging request for movie-456
Streaming movie-456 in 4K
Blocked: Only PREMIUM users can play videos

What just happened? The proxy intercepted every call. It checked the user's role, checked the cache, logged the call, and — only on the first request — created the heavy RealVideoService. The client never saw any of this logic. The real service stayed clean.

For Beginners: The Security Guard Analogy

Think of an office building. You don't walk straight into every office. You pass through a security guard first. The guard checks your ID, logs your visit, and only then lets you in. The guard doesn't do your work — but they control whether you get to the person who does.

Proxy is the security guard. The real subject is the employee inside. The client is you, trying to reach the employee.

For Experts: Proxy vs. Decorator vs. Adapter

These three patterns all involve a wrapper. This is the classic interview trap.

Aspect Proxy Decorator Adapter
Intent Control access to an object Add behavior to an object Convert one interface to another
Interface Same as real subject Same as wrapped object Different — makes the new one
Adds logic? Around — not inside Yes, before/after Yes, translates calls
Knows the real object? Yes — creates or holds it Yes — wraps it Yes — converts it
Focus Access, lifecycle Behavior Compatibility

Rule of thumb: Proxy is the bouncer. Decorator is the costume designer. Adapter is the translator.

Putting It All Together: Real Java Examples

The Proxy pattern isn't just academic. You use it every day in Java — often without knowing it.

Example 1: Spring AOP (@Transactional)

When you annotate a method with @Transactional, Spring wraps that bean in a dynamic proxy. The proxy opens a transaction, calls your method, then commits or rolls back. Your method never sees the transaction management code.

@Service
public class OrderService {
    @Transactional
    public void placeOrder(Order order) {
        // Only business logic here
        repository.save(order);
    }
}

Example 2: Spring @Cacheable

@Cacheable works the same way. Spring wraps the bean in a proxy. When you call the method, the proxy checks the cache first — and only calls your method if the cache misses.

Example 3: Hibernate Lazy Loading

When you load an Order with a @ManyToOne Customer, Hibernate gives you a proxy for the customer. Only when you call order.getCustomer().getName() does the proxy hit the database.

Example 4: Java's Dynamic Proxy

java.lang.reflect.Proxy lets you create proxies at runtime without writing a class. This is how Spring AOP works under the hood.

The Proxy Structure

Framework What the Proxy Does
Spring @Transactional Opens and commits transactions around your method
Spring @Cacheable Checks cache before calling your method
Spring @PreAuthorize Checks user roles before calling your method
Hibernate lazy loading Fetches data on demand instead of at load time

Personal note: I once had a service that loaded a huge configuration file from S3 on every request. The service was slow and expensive. Instead of changing the client code, I added a proxy that cached the config for 5 minutes. Clients kept calling the same method — the proxy silently cut S3 calls by 99%. The real service didn't change at all.

Frequently Asked Questions

What is the Proxy design pattern in Java?

Proxy is a structural pattern that provides a substitute or placeholder for another object. The proxy implements the same interface as the real object, so clients interact with it transparently. It controls access by adding logic before or after delegating to the real object.

When should I use the Proxy pattern?

Use Proxy for lazy loading (create expensive objects only when needed), access control (check permissions before delegating), caching (return cached results for repeated requests), or logging (record every call). It is also used for remote proxies (RPC) and smart references.

How is Proxy different from Decorator?

Decorator adds new behavior to an object. Proxy controls access to an object. Both wrap the original and share its interface. The key difference: Decorator is about augmenting behavior, Proxy is about managing access and lifecycle.

How is Proxy different from Adapter?

Adapter changes an object's interface to make it compatible with a client. Proxy keeps the same interface and adds control logic. Adapter solves incompatibility; Proxy solves access and lifecycle.

Where is Proxy used in real Java code?

Spring AOP uses proxies for @Transactional and @Cacheable. Hibernate uses proxies for lazy-loaded entities. Java's java.lang.reflect.Proxy enables dynamic proxies. RMI uses remote proxies to call methods on another JVM.

Your Proxy Checklist

Before adding guards to your real object, ask:

  • Am I repeating the same guard logic in every client? If yes, move it into a proxy.
  • Is the real object expensive to create? A virtual proxy can delay creation until it's actually needed.
  • Do I need access control? A protection proxy checks permissions before delegating.
  • Are repeated calls expensive? A caching proxy stores the result for next time.
  • Do I need to log every call? A logging proxy records every invocation without touching business logic.

Start small. Wrap one expensive or guarded object in a proxy. The clarity compounds.

Conclusion

The Proxy pattern isn't about hiding the real object — it's about controlling access. When clients need the same guard logic (permissions, caching, logging, lazy loading), Proxy gives them one stand-in that handles it all. It trades a small upfront design decision for long-term cleanliness in the real object.

I'll be honest: I don't use Proxy in every project. Small services with one client don't need it. But for anything that involves permissions, expensive resources, or repeated calls — payments, config loading, video streaming, database entities — it has saved me from duplicating the same guards in twenty places. Start with the problem, not the pattern. If your client code is checking permissions or cache before calling a method, Proxy is probably the answer.

Related topics: SOLID Principles in Java, Decorator Design Pattern, Adapter Design Pattern, Facade Design Pattern, Spring AOP, Structural Design Patterns