Proxy Pattern in Java and Spring: A Complete Guide
In modern backend systems, adding features such as transaction management, logging, caching, or security without cluttering the business…
Proxy Pattern in Java and Spring: A Complete Guide
In modern backend systems, adding features such as transaction management, logging, caching, or security without cluttering the business logic is critical.
This is exactly where the Proxy Pattern becomes powerful.
Instead of modifying existing classes, we introduce a middle layer that controls how objects are accessed. This allows us to extend behavior without touching core logic, a principle heavily used in Spring.
In this article, we’ll break down:
- How the Proxy Pattern works in plain Java
- How Spring uses it under the hood
- And the hidden pitfalls every developer should know
Let’s get started! 🚀

1. Understanding the Idea (Manager Example)
Imagine a company where an important manager makes all critical decisions.
You don’t directly go to the manager. Instead:
- You (employee) → make a request
- Assistant → checks, logs, filters
- Manager → does the actual work
This is exactly how the Proxy Pattern works.
The assistant acts as a proxy, controlling access to the real object (manager) and optionally adding extra behavior.

👉 In software terms: A proxy is a middle layer that controls access to another object that performs the actual work.
2. Core Concepts
The Proxy Pattern is a structural design pattern that consists of three main components:
2.1. Subject (Interface)
Defines what operations are available, a contract followed by both the real object and the proxy.
public interface EmployeeService {
void createLeaveRequest();
void sendReport();
}
👉 It answers: “What can this system do?”
2.2. Real Subject (Manager)
This does the actual work.
- Executes business logic
- Makes decisions
public class Manager implements EmployeeService {
public void createLeaveRequest() {
System.out.println("Manager approves leave");
}
public void sendReport() {
System.out.println("Manager processes report");
}
}
2.3. Proxy (Assistant)
Controls access and adds behavior before delegating to the real subject:
- Checks requests
- Logs actions
- Applies rules
- Then forwards the call
public class AssistantProxy implements EmployeeService {
private final Manager manager;
public AssistantProxy(Manager manager) {
this.manager = manager;
}
public void createLeaveRequest() {
System.out.println("Assistant checks request");
manager.createLeaveRequest();
}
public void sendReport() {
System.out.println("Assistant logs report");
manager.sendReport();
}
}
Flow
Client → Proxy → Real Object
This separation improves control, flexibility, and maintainability.
3. Practical Java Example
Let’s apply this to a simple bank transfer system.
// Subject
public interface BankService {
void transfer(String from, String to, double amount);
}
// Real Subject
public class BankServiceImpl implements BankService {
public void transfer(String from, String to, double amount) {
System.out.println("Transferring " + amount);
}
}
// Proxy
public class BankServiceProxy implements BankService {
private final BankService realService;
public BankServiceProxy(BankService realService) {
this.realService = realService;
}
public void transfer(String from, String to, double amount) {
System.out.println("LOG: request received");
// extra behaviors.. amount checks, some rules, etc.
realService.transfer(from, to, amount);
System.out.println("LOG: request completed");
}
}
// Client
public class Main {
public static void main(String[] args) {
BankService service = new BankServiceProxy(new BankServiceImpl());
service.transfer("Alice", "Bob", 1200);
}
}
Key Takeaways
- Proxy adds additional behavior (logging, validation, etc.)
- Real service remains clean and focused on business logic
- Responsibilities are clearly separated
4. Proxy Pattern in Spring
In Spring, proxies are not optional; they are fundamental.
This mechanism is powered by AOP (Aspect-Oriented Programming), which separates cross-cutting concerns like transactions, logging, and caching from business logic.
When you use annotations like:
- @Transactional
- @Cacheable
- @Async
Spring does not modify your class.
Instead, it creates a proxy object that wraps your original bean and intercepts method calls, applying AOP advice (e.g., before, after, around) around the actual method execution.
Execution Flow
Client → Proxy → Real Method → Proxy → Client
Transaction Example
@Service
public class PaymentService {
@Transactional
public void makePayment() {
System.out.println("Processing payment...");
}
}
Behind the scenes, Spring effectively does:
startTransaction();
callRealMethod();
commitOrRollback();
5. Spring Proxy Types
Spring mainly uses two types of proxies:
5.1 JDK Dynamic Proxy (Interface-Based)
Used when the class implements an interface.
- Spring creates a separate proxy object
- The proxy implements the same interface
So when you call:
bankService.transfer();
👉 You’re not calling BankServiceImpl directly, you’re calling the proxy; just like in the manager example above.
5.2 CGLIB Proxy (Class-Based)
Used when there is no interface.
- Creates a runtime-generated subclass
- Overrides methods to inject behavior
- Works directly on the concrete class
Example
public class PaymentService {
public void pay() {
System.out.println("Payment completed");
}
}
Spring generates something like this behind the scenes:
class PaymentServiceProxy extends PaymentService {
@Override
public void pay() {
System.out.println("Before method");
super.pay();
System.out.println("After method");
}
}
Rule of Thumb
- Interface exists → JDK Proxy
- Otherwise → CGLIB Proxy
For a quick overview, you can take a look at the diagram below.

6. The Self-Invocation Trap
This is one of the most common and subtle issues in Spring.
@Service
public class OrderService {
@Transactional
public void createOrder() {
saveOrder(); // internal call
}
@Transactional
public void saveOrder() {
System.out.println("Saving order");
}
}
What Happens?
You might expect saveOrder() to run within a transaction by making internal calls.
But it does not. 😀
Why?
Because proxies only intercept external calls. 🚀
👉 this.saveOrder()
- Direct call
- Bypasses proxy
- No transaction applied
How to Fix
- Move method to another bean → ensures call goes through proxy
- Inject self proxy → call via proxy instead of this
- Use AspectJ → applies aspects even for internal calls
Golden Rule
- ✅ External calls → intercepted, go through proxy
- ❌ Internal calls → ignored, bypass proxy
7. Summary
The Proxy Pattern is a core concept in both Java and Spring:
- Acts as a middle layer between the client and the real object
- Adds cross-cutting concerns like transactions, logging, and caching
- Keeps business logic clean and focused
- Improves modularity and maintainability
In Spring, proxies are everywhere, even if you don’t see them.
👉 Understanding them helps you:
- Avoid subtle bugs (like self-invocation issues)
- Write more predictable code
- Better understand what happens at runtime
🔍 Best Practices
- Use proxies for cross-cutting concerns (transaction, logging, caching, security)
- Avoid overusing them for simple logic
- Be aware of proxy limitations (especially internal calls)
Thank you for reading! 🎉 I hope this guide helped you understand the Proxy Pattern and how it works in Spring.
I’d love to hear your thoughts or feedback - feel free to reach out! 😇
[embed]
메타데이터
- post_id
- da1d1637c62e
- slug
- proxy-pattern-in-java-and-spring-a-complete-guide-da1d1637c62e
- url
- https://medium.com/@zeynepsjourney/proxy-pattern-in-java-and-spring-a-complete-guide-da1d1637c62e
- canonical_url
- https://medium.com/@zeynepsjourney/proxy-pattern-in-java-and-spring-a-complete-guide-da1d1637c62e
- author_url
- https://medium.com/@zeynepsjourney
- status
- ok
- fetched_at
- 2026-07-13 09:15:10