Calling a service's reserve() method directly prints the @Around log. Calling reserve() from the same service's checkout() method does not. reserve() still runs; the internal call does not pass through the proxy again, so that method's advice has no opportunity to run.
Which calls can Spring AOP intercept?
The diagram compares the call paths. An external call to the proxied bean enters reserve() advice. A call through this.reserve() inside the target object does not cross that proxy boundary again.
Spring AOP places a proxy in front of a target bean. Advice such as @Around can wrap calls entering through that proxy. The example below assumes an @Around pointcut matching reserve(); it shows the service structure rather than a complete aspect configuration:
@Service
public class CouponService {
public void checkout() {
reserve(); // implicit this.reserve(): same target object
}
public void reserve() {
// Reserve a coupon.
}
}When a controller calls reserve() on the injected CouponService, the path is proxy → advice → reserve(). When execution has already entered checkout() and calls reserve() on the same target object, it does not re-enter the proxy. Advice matched only to reserve() is therefore skipped on that internal call. Advice matched to the external checkout() call is a separate question. Spring's proxying reference states this self-invocation rule explicitly.
How should you narrow down missing advice?
Observe method execution and advice execution separately. If the method runs but advice does not, compare these paths:
- Did the caller use the bean injected by Spring?
- Is the call from
checkout()toreserve()a call on the same object? - Does the pointcut actually match the method and scope you expect?
An integration test could call the injected bean's reserve() and checkout() separately, then count advice matched only to reserve(). The expected counts are one for the external reserve() path and zero for the self-invocation path. These are predictions from proxy behavior, not claimed logs from a running Spring project.
A manually constructed object is another case
new CouponService() is not the proxied Spring bean. If advice is missing on that object, identify the object being called before investigating self-invocation. A call on an unproxied object and a self-invocation inside a proxied bean have different causes, even though both can appear as missing advice.
What changes when another bean owns the work?
If reservation is genuinely an independent responsibility, move it to a ReservationService bean and inject that into CouponService. Then checkout() calls another bean's proxy rather than its own target method:
@Service
public class CouponService {
private final ReservationService reservations;
public CouponService(ReservationService reservations) {
this.reservations = reservations;
}
public void checkout() {
reservations.reserve();
}
}This is a good design when the responsibilities really differ, not a reason to turn every small helper into a new bean. If advice enforces a transaction or security boundary, however, do not ignore a path that bypasses it. Calling the current proxy through AopContext is possible but couples business code to AOP configuration; consider the design boundary first.
Key takeaways: Follow the call path
Spring AOP advice applies to calls through the proxy. A this.reserve() call inside the same object can run the method while bypassing its advice. Compare external and internal calls, then decide whether the advised behavior belongs at another bean boundary.

