HOW PROXY AND
CONSTRUCTOR INJECTION WORKS
IN SPRING BOOT
As Spring Boot developers, we write annotations like @Service, @Transactional, or @Async every day. But have you ever wondered how Spring magically adds transactions, retries, caching, or async behavior to your methods, even though your code never explicitly does those things?
The answer is simple: Spring uses proxies.
But what exactly is a proxy? How does it work? And why do we need to care?
What is Proxy in Spring
What is a Proxy in Spring? A proxy is a wrapper object that Spring creates around your bean. It looks like your bean, acts like your bean… but isn’t actually your bean.
A proxy is a mechanism to implement AOP in Spring. Spring uses proxies to wrap your bean and execute the aspect code before/after/around your method. This is the core idea of AOP (Aspect-Oriented Programming).
When you write:
paymentService.process();
You think you're calling the real object, but you are actually calling:
paymentServiceProxy.process();
The flow becomes:
proxy.process()
→ start transaction
→ call real paymentService.process()
→ commit/rollback transaction
→ return resultThat proxy intercepts your method call and adds functionality like:
- check_circle Starting/ending transactions
- check_circle Async execution
- check_circle Caching
- check_circle Retries
- check_circle Security checks
- check_circle Logging
- check_circle Metrics
Your method gets more power without changing a single line of your business code. That is the magic.
Why Spring Needs Proxies
Spring needs a place to “insert” additional behavior around your method.
For example:
@Transactional
public void transferMoney() { ... }When using transactions, Spring must:
- check_circle Open transaction
- check_circle Call your method
- check_circle Commit or rollback
- check_circle Close transaction
Your method alone can't do that, so the proxy handles it.
Example: A Simplified Transaction Proxy
Conceptually, Spring generates something like:
class PaymentServiceProxy implements PaymentService {
private final PaymentService target;
@Override
public void process() {
beginTransaction();
try {
target.process();
commit();
} catch (Exception e) {
rollback();
throw e;
}
}
}Your actual class:
@Service
public class PaymentService {
public void process() {
// business logic
}
}Your code remains clean, while the proxy injects transactional behavior.
How Spring Decides Which Proxy Type to Use
Spring uses 2 kinds of proxies:
1. JDK Dynamic Proxy
Used when your service implements an interface.
proxy --> interface --> real class
public interface PaymentService { ... }
public class PaymentServiceImpl implements PaymentService { ... }2. CGLIB Proxy
Used when your service class has no interface.
PaymentService extends PaymentService$$SpringProxy
@Service
public class PaymentService { ... }In this case, Spring creates a subclass behind the scenes.
The Proxy Trap: Self-Invocation
One of the biggest mistakes developers run into:
@Service
public class MyService {
@Transactional
public void methodA() { ... }
public void methodB() {
methodA(); // ❌ proxy is bypassed
}
}methodA() is called directly → transaction NOT applied.
Why does this happen?
Because Spring proxy only intercepts external method calls.
Internal calls inside the same class skip the proxy and your @Transactional won’t work.
This is why understanding proxies matters in real life.
Constructor Injection Ensures Proxy Safety
Modern Spring Boot recommends:
@Service
@RequiredArgsConstructor
public class MyService {
private final PaymentService paymentService;
}1. @Service → Register as a Spring Bean
@Service tells Spring:
- check_circle Create one instance of MyService (singleton by default)
- check_circle Manage its lifecycle
- check_circle Wrap it with proxy if needed (AOP, @Transactional, etc.)
So MyService becomes a Spring-managed bean, not a normal Java object.
2. @RequiredArgsConstructor (Lombok)
This generates a constructor:
public MyService(PaymentService service) {
this.service = service;
}
Because the field is: private and final
Lombok auto-generates a constructor that Spring uses for dependency injection.
You do NOT need @Autowired.
3. Spring Injects PaymentService (Also a Bean)
private final PaymentService service;Meaning Spring will find:
@Service
public class PaymentService { ... }
and inject the proxied instance of PaymentService.
So internally:
MyService ----> proxy(PaymentService)
You are not manually creating PaymentService, and you are not manually creating MyService.
Both are created by Spring and injected through the constructor.
This means:
- check_circle You are always receiving a proxied version of the bean because dependency must be provided, otherwise the app won’t start.
- check_circle Dependencies are explicit. No chance of NPE due to missing injection because Spring MUST provide it.
- check_circle No hidden field injections. Just by looking at constructor / final fields, you know exactly what the class needs.
- check_circle No lifecycle surprises.
- check_circle No possibility of bypassing the proxy via new MyService().
- check_circle Immutable. final ensures no one can change the dependency after construction.
If you wrote:
@Service
public class MyService {
@Autowired
private PaymentService paymentService;
public void pay() {
paymentService.process();
}
}The dependency is “hidden” and errors can happen silently.
1. Cannot create instance outside Spring (not test-friendly)
If you try to create an object manually (unit test, utility, etc.):
MyService s = new MyService();
s.pay(); // paymentService == null → NullPointerException
Because paymentService is injected only when Spring runs the container.
You cannot see this issue from the class definition. It’s hidden.
2. You can accidentally miss an injection → NPE at runtime
Field injection hides required dependencies. There is no constructor, so you have no idea what this service actually depends on unless you read all fields.
If someone removes @Autowired accidentally:
private PaymentService paymentService; // no autowiredSpring still compiles fine → but at runtime, you get NPE.
3. Circular dependencies are harder to detect
Field injection hides the dependency graph, so circular reference bugs are harder to diagnose.
Constructor injection fails fast, at startup.
Field injection fails later, sometimes during runtime.
4. final cannot be used → no immutability
With field injection:
@Autowired
private PaymentService service;
You cannot make it final
Meaning the internal dependency can change (mutable object), which is unsafe.
Constructor injection forces it to be final → safer.
5. Field injection is harder for static analysis
Static code analysis tools, IDE inspections, and SonarQube cannot reliably detect missing dependencies.
With constructor injection, your dependencies are explicit:
public MyService(PaymentService paymentService) { … }Easy for tools, easy for humans.
6. Proxies can fail silently
Field injection sometimes injects the wrong thing (raw object instead of proxied bean) if misconfigured.
Constructor injection is more predictable.
7. Hidden lifecycle issues in AOP
If you use field injection with things like:
- check_circle @Transactional
- check_circle @Async
- check_circle @Cacheable
Sometimes the proxy is not fully ready when the object is created, causing subtle bugs.
You don’t need to be an expert in AOP to use Spring effectively. But knowing how proxies work and how they affect your code, will make you a significantly better Spring Boot developer.
If you’re writing services, using @Transactional, or building microservices with complex flows, this knowledge is essential.