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 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