A proxy is like a middleman that sits between you and the actual object (a Spring bean). It adds extra functionality, like logging or transaction management, before or after calling the real method.
π In simple words: A proxy is a helper that wraps your code to add special behavior.
Spring uses proxies to add "magic" to your code without you needing to write it. For example:
πTransactions (@Transactional): Ensures database operations are saved or rolled back correctly.
πAsync execution (@Async): Runs methods in a separate thread for better performance.
πCaching: Stores results to avoid repeating work.
πSecurity: Checks permissions before running a method.
πLazy Loading: Loads data only when needed (e.g., in Hibernate).
βͺοΈ How it works: When you call a method on a Spring bean, youβre actually calling the proxy. The proxy decides what to do before or after the real method runs.
Spring creates proxies in two ways:
| Proxy Type | When Used | How It Works |
|---|---|---|
| JDK Dynamic Proxy | If your class implements an interface | The proxy pretends to be the interface. |
| CGLIB Proxy | If your class doesnβt have an interface | The proxy creates a subclass at runtime. |
Tip: Always use interfaces (e.g., MyService) to make proxies work smoothly.
sequenceDiagram
Controller ->> Proxy: Call myMethod()
Proxy ->> Advice: Add extra logic (e.g., @Transactional)
Advice ->> RealBean: Run the actual method
RealBean -->> Advice: Return result
Advice -->> Proxy: Apply more logic if needed
Proxy -->> Controller: Send final result
The diagram shows how a method call flows in Spring Boot using a proxy.
The Controller starts by calling a method, which goes to the Proxy first.
The Proxy passes it to Advice to add extra logic (like @Transactional), then to the RealBean to execute.
The result returns through Advice and Proxy back to the Controller.
| Problem | Why It Happens |
|---|---|
| Self-invocation | Calling a method from the same class (e.g., this.myMethod()) skips the proxy |
| Private methods | Proxies can't intercept private methods |
| Final methods | Proxies can't override final methods |
| Non-Spring beans | If you create an object with new, Spring canβt add a proxy |
| Direct calls | Proxies only work when called from outside the class |
π Example β Self-invocation fails:
@Service
public class MyService {
@Transactional
public void outerMethod() {
this.innerMethod(); // β Proxy is bypassed β no transaction!
}
@Transactional
public void innerMethod() {
// DB operations here
}
}- What it does: Manages DB transactions (commit/rollback).
- How it works: Proxy starts transaction before the method, and commits/rolls back afterward.
β οΈ Works only when method is called from outside the class.
π Service: Service Using @Transactional
β Doesnβt work if:
- Method is
privateorfinal - Called inside same class
- What it does: Runs method in a different thread to speed things up.
- How it works: Proxy hands off method to thread pool executor.
π Async Service: Async Service
β Doesnβt work if:
- Method is called via
this.sendEmail()inside same class - Bean is not managed by Spring
- Exception is thrown before returning
CompletableFuture
π Using custom thread pool: custom thread pool
@Service
public class MyService {
@Async("customExecutor")
public void sendEmail() {
// Runs with custom thread pool
}
}| Scenario | Proxy Works? | Why |
|---|---|---|
this.myMethod() |
β | Skips proxy |
| Method called from controller | β | Goes through proxy |
@Async with CompletableFuture |
β | Proxy handles it correctly |
| Exception thrown before return | β | Proxy can't catch it β no async behavior |
flowchart TD
A[Controller] --> B[Proxy]
B --> C[Extra Logic]
C --> D[Real Bean]
D --> C
C --> B
B --> A
sequenceDiagram
Controller ->> Proxy: Call @Async method
Proxy ->> ThreadPool: Submit task
ThreadPool ->> NewThread: Run method
NewThread -->> RealBean: Do the work
The diagram shows how an @Async method works in Spring Boot.
The Controller calls the method, and the Proxy submits it to the ThreadPool.
The ThreadPool assigns it to a NewThread, which runs the method in the RealBean.
This allows the task to run in the background without blocking the Controller.
Lazy Loading means:
Hibernate will not load related data (e.g., collections or associations) until you access it.
Instead of returning the real data (like a list of books), Hibernate returns a proxy object that delays loading until needed.
π Example:
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
Author author = repo.findById(1L).orElseThrow();
// No query for books yet
author.getBooks(); // triggers SQLHibernate creates a proxy class (like PersistentBag) that:
- Delays loading actual data
- Holds reference to the Hibernate Session
- Loads data when
.size(),.get(), or.iterator()is called
| Situation | Works? | Reason |
|---|---|---|
Inside @Transactional |
β | Session is still open |
Inside controller (open-in-view) |
β | Session kept open for request |
| After transaction ends | β | Session closed = proxy fails |
| After entity serialization | β | Session reference lost in JSON |
Hibernate proxies depend on an active Session. If closed:
System.out.println(author.getBooks().size());You get:
LazyInitializationException: could not initialize proxy β no SessionInternally:
if (this.session == null || !this.session.isOpen()) {
throw new LazyInitializationException("No Session");
}sequenceDiagram
participant C as Controller
participant S as Service
participant H as Hibernate
participant DB as Database
C ->> S: loadAuthor(id)
S ->> H: findById(id)
H ->> DB: SELECT * FROM authors WHERE id=?
H -->> S: returns Author + Lazy Proxy (books)
S -->> C: returns Author
C ->> Author: getBooks().size()
alt Session is open
H ->> DB: SELECT * FROM books WHERE author_id=?
H -->> Author: returns books
else Session closed
H ->> Exception: LazyInitializationException
end
The sequence diagram shows how Lazy Loading works:
The Controller asks the Service to load an Author by id. The Service uses Hibernate to fetch the Author from the Database, returning it with a Lazy Proxy for books. When getBooks().size() is called, if the Session is open, Hibernate fetches the books data; if closed, it throws a LazyInitializationException.
| Strategy | Description |
|---|---|
@Transactional |
Keeps the Hibernate Session open during the operation, allowing the Lazy Proxy to fetch data safely when needed. |
join fetch |
Fetches related data (e.g., books) along with the parent (e.g., Author) in a single database call, avoiding multiple queries |
| DTO projection | Fetch only what you need: Creates a custom object with just the required fields, bypassing the Lazy Proxy and preventing session issues. |
@EntityGraph |
Control eager loading declaratively: Defines which relationships to load eagerly using annotations, giving fine-tuned control over data fetching. |
open-in-view=true |
(Caution) Keeps session open for web requests: Extends the Hibernate Session throughout the web request, but it may impact performance. |
fetch = EAGER |
Always load with parent (not recommended for large collections): Forces Hibernate to load related data immediately with the parent, which can slow down the app if the data is big. |
Spring provides a powerful event-driven model that allows components to communicate without being tightly coupled.
But under the hood, it relies on proxies and Spring-managed beans to work properly.
- A publisher can fire an event using
ApplicationEventPublisher. - Any listener annotated with
@EventListenerwill handle the event. - Add
@Asyncon listeners to process events asynchronously in a different thread.
For @EventListener and @Async to work:
β
The listener must be a Spring-managed bean (e.g., annotated with @Component, @Service, etc.).
If you create the object manually using new, no proxy is created, and it wonβt work.
π Event Class Event Class
π Valid Listener (Spring-managed) Valid Listener
This works because Spring wraps the bean in a proxy, enabling both
@EventListenerand@Async. Note:@Asyncmakes the listener run on a separate thread (e.g.,pool-1-thread-1).
π Invalid Listener (Manual object β No Proxy) Invalid Listener
Even if defined as a
@Bean, it wonβt work unless it's also proxied (e.g., via@EnableAspectJAutoProxy).
π Event Publisher Event Publisher
| Feature | Needs Proxy | Works with new? |
Works with @Bean? |
|---|---|---|---|
@EventListener |
β Yes | β No | β No (unless proxied manually) |
@Async |
β Yes | β No | β No (unless proxied manually) |
@EventListenerand@Asyncare implemented via Spring AOP.- Without a proxy, Spring wonβt detect or intercept the method.
- The proxy handles both event dispatching and thread management.
Spring AOP is a powerful tool in the Spring Framework that helps you handle cross-cutting concernsβlike logging, security, or performance trackingβwithout cluttering your main business logic. Instead of repeating code in many places, AOP lets you write it once and apply it when needed.
- Keep Code Clean: Keeps extra responsibilities separate from the core logic.
- Promote Reusability: Write once, reuse in multiple places (e.g., logging).
- Simplify Maintenance: Update concerns like security or logging in one place.
- Add Flexibility: Enable/disable behaviors without touching core business code.
- Service Layer: Transaction management, business event logging
- Controller Layer: Security checks, request tracking
- Repository/Data Layer: Retry logic, performance monitoring
- You need to apply the same action to multiple methods (e.g., logging, auth).
- You need centralized security, performance, or transaction logic.
- You want centralized error handling or logging.
- Logging: Record when a method starts, ends, or fails.
- Security: Restrict access to specific users/roles.
- Transactions: Ensure database operations are consistent.
- Error Handling: Log and handle exceptions globally.
AOP weaves extra logic (advice) into your existing code at specific points (join points). Below are the core concepts:
- Aspect: A class that contains cross-cutting logic.
- Advice: Code that runs at a specific point (e.g., before a method).
@Before,@AfterReturning,@AfterThrowing,@After,@Around
- Pointcut: A rule for where the advice should apply.
- Join Point: The actual execution point (e.g., method call).
- Target: The object being advised (e.g., your service/controller).
Client
β
βΌ
[Proxy Object] <-- Spring creates this
β
ββββββββ΄βββββββββββββ
β Check Pointcut β
β β³ Is it a match? β
ββββββββ¬βββββββββββββ
β Yes
βΌ
Apply Advice
(before, around, after...)
β
βΌ
Call Real Method
Spring AOP uses proxies to intercept method calls and apply aspects without modifying your original code.
To activate AOP in your project, you need:
- Configuration to enable proxying
- An
@Aspectclass - Target methods with matching pointcuts
In Spring, proxies act as intermediaries to enable features like @Transactional, @Async, and AOP. While this adds flexibility and modularity, it introduces a performance cost that developers should be aware of.
| Issue | Explanation |
|---|---|
| Increased Execution Time | Every method call on a proxied Bean goes through additional steps (e.g., executing Advice or checking transactions), causing a slight delay. This is more noticeable with multiple proxies (Proxy Chain). |
| Memory Overhead | Proxies create additional objects (e.g., JDK Dynamic Proxy or CGLIB subclasses), consuming more memory, especially with many proxied Beans. CGLIB proxies, which create subclasses, use more memory than JDK proxies. |
| Initialization Overhead | During application startup, Spring takes time to create proxies for all Beans requiring them. In large applications with hundreds or thousands of Beans, this can slow down startup time. |
| Self-Invocation Issues | Calling a method within the same class (this.method()) bypasses the proxy, which can lead to unexpected behavior (e.g., no transaction applied). This not only breaks functionality but also costs debugging time. |
| Lazy Loading in Hibernate | Hibernateβs Lazy Proxy saves performance by deferring data loading, but accessing it after the Session closes causes a LazyInitializationException. This often forces developers to use open-in-view=true, which keeps the Session open longer and increases resource usage. |
While the performance cost of proxies is usually acceptable for most business applications, it's essential to monitor and optimize when building high-performance, real-time, or resource-sensitive systems.
-
Use Interfaces:
Prefer JDK Dynamic Proxy (used with interfaces) over CGLIB, as itβs lighter on performance since it doesnβt create subclasses or modify bytecode extensively. -
Minimize Advice Usage:
Avoid stacking multiple annotations (e.g.,@Transactional,@Async,@Cacheable) on the same method, as each adds a proxy or Advice, increasing overhead. -
Avoid Self-Invocation:
To ensure proxies are applied, inject the Bean itself via Dependency Injection instead of calling methods withthis.