prev: [What `@Async` actually does](01-what-async-actually-does.md) · [README](../README.md) · next: [What the proxy cannot see](03-what-the-proxy-cannot-see.md) # 2. The self-invocation trap The proxy wraps the bean. It does not replace `this`. ```java @Async public CompletableFuture annotated() { ... } public CompletableFuture viaSelfInvocation() { return annotated(); // this.annotated() — the proxy is not involved } ``` When another bean calls `service.annotated()`, it holds a reference to the *proxy*, so the call is intercepted. When `viaSelfInvocation()` calls `annotated()`, the compiler emits `this.annotated()`, and `this` inside the method body is the *target* object, not the proxy. There is no interception, so the method runs inline and the `CompletableFuture` you get back is already complete. Measured, in [`docs/output/self-invocation.txt`](output/self-invocation.txt): ``` service.annotated() : task-5 (virtual=false) service.viaSelfInvocation() : main (virtual=false) service.viaSelfReference() : task-6 (virtual=false) ``` Nothing is logged. The method returns a valid future with the correct value in it. The only symptom is that the caller waited. ## The fixes, in the order you should prefer them **Move the method to another bean.** If work is asynchronous, it belongs behind a boundary, and the boundary is a good place for a class. This is the fix that survives a refactor. **Go back out through the proxy on purpose.** `SelfInvocationService` does this with an `ObjectProvider`: ```java public CompletableFuture viaSelfReference() { return self.getObject().annotated(); } ``` `ObjectProvider` is lazy, so there is no circular dependency to resolve at construction time. Injecting `SelfInvocationService` directly also works — Spring special-cases self-references — but it looks like a bug to every reviewer who has not read this chapter. **`@EnableAsync(mode = AdviceMode.ASPECTJ)`** removes the limitation entirely, because load-time weaving rewrites the method itself rather than wrapping the object. It also adds an agent to your startup and a weaving configuration to your build. Very few applications should pay that. ## Why this keeps happening The trap is not that people do not know about proxies. It is that the calling code often did not start out in the same class. A method gets extracted, a controller's logic moves into the service that already had the `@Async` method, and one day a call that used to arrive from outside arrives from inside. Nothing fails. The throughput just changes. next: [What the proxy cannot see](03-what-the-proxy-cannot-see.md)