Build1 distinct publisher3 min readUpdated
Spring's annotations run in a generated wrapper around your bean, not inside your method. Which wrapper you get, and how callers reach it, decides whether the behaviour fires.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A dev.to write-up on Spring proxies restates something worth keeping on a whiteboard: `@Transactional` does not open your transaction and `@Cacheable` does not skip your method body [1][2]. A generated stand-in object sitting between the caller and the bean does both, which means any call path that misses the stand-in runs the plain method body and returns normally with none of the behaviour attached [3][1].
The shape is the old wrapper pattern. Write a second class that implements the same interface, hold the real object as a field, add your logging or your transaction handling, then forward the call; callers cannot tell the difference [4]. Spring's only twist is that you do not write the wrapper. It manufactures one at runtime for a class it has never seen [5]. The reason is bookkeeping: transactions, security, caching and metrics cut across many unrelated methods, so the annotation leaves your method body clean and the behaviour lives in the wrapper instead [6]. That is the whole bargain, and it is also the failure mode.
There are two ways Spring builds the wrapper, and they fail differently. The first is `java.lang.reflect.Proxy`, shipped with Java since 1.3: hand it a set of interfaces and it generates, in memory, a new class that implements them [7]. Every call on that object funnels into one `InvocationHandler`, which receives the method and args, does the extra work, and forwards with `method.invoke(real, args)` [8]. The constraint is in the argument list. A JDK dynamic proxy can only mimic interfaces; the generated class implements `PaymentService` but is not a `RealPaymentService`, just a synthetic class sharing the surface, so the mechanism works only when the bean has an interface to stand behind [9]. Anything in your codebase that casts to the concrete class, or checks for it, is not looking at that object's type [2].
The second is CGLIB, which Spring reaches for when the bean has no interface. It generates a subclass of your class at runtime and overrides the methods [10]. Interception is therefore override-based, which means a method a subclass cannot override cannot carry proxied behaviour [3]. Nothing about the annotation changes; the hook simply has nowhere to attach.
The trap that costs the most is neither of those. In the pattern the article describes, the wrapper holds the target, and the target holds no reference back to the wrapper [4][3]. So when a bean calls one of its own annotated methods, the call goes straight to the body and never passes through the proxy [4]. The transaction does not open. The cache is not consulted. The method returns a correct-looking value [1].
Three checks are worth doing before you trust an annotation in a hot path. Whether each annotated bean has an interface, because that determines which mechanism you got [9][10]. Whether the annotated method is ever reached from inside its own class rather than through the injected reference [4]. And whether any method you annotated is one a generated subclass could not override [3]. All three are cheap to establish and none of them announce themselves at runtime.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
Putting @Transactional on a method causes a database transaction to open before the method runs and commit when it returns, even though the developer wrote no line of code to start or end one.
With @Cacheable, the second call with the same arguments skips the method body entirely.
The extra behaviour runs inside a proxy: a stand-in object Spring slips between whoever calls the bean and the real bean itself. Proxies appear with any Spring annotation that wraps behaviour around a method, including transactions, caching, security, retries and async.
The hand-written equivalent is a second class that implements the same interface, holds the real object (the target) as a field, adds the extra behaviour and forwards the call to the target; callers cannot tell the difference because it is the same interface type.
Spring manufactures the wrapper for you at runtime, for a class it has never seen before, rather than requiring you to write it.
A behaviour that cuts across many unrelated methods is a cross-cutting concern; transactions, security checks, caching and metrics are all cross-cutting. Spring's answer is to keep the method body clean and push the concern into the proxy, so the annotation's behaviour lives in the wrapper.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Checkable API mechanics, single unsourced explainer
The substantive claims are about named, publicly verifiable APIs - java.lang.reflect.Proxy since Java 1.3, InvocationHandler dispatch with method.invoke(real, args), CGLIB Enhancer.setSuperclass plus invokeSuper - and are demonstrated with runnable snippets, which raises verifiability above a typical opinion post. It is nonetheless one community publisher with no links to Spring, CGLIB or JDK documentation, no version matrix, and a body truncated before the pitfalls section, so the derived consequences are entailed from the described mechanism rather than stated and tested.
Default-path framework mechanism, no usage data
Adoption here is structural rather than reported: the JDK proxy mechanism has shipped in the Java standard library since 1.3, and the source describes CGLIB subclass proxying as Spring Boot's default for every bean since 2.0, so proxy interception sits on the default path of essentially any annotation-using Spring application. The cluster supplies no deployment counts, telemetry, benchmarks or user reports, so the score reflects default-path ubiquity of a mature mechanism, not measured usage.
Slightly sharp framing, mechanics track the claims
The framing that the proxy 'decides whether @Transactional does anything at all' is dramatic but consistent with the mechanism described, and the piece sells nothing, promises no performance win and names its own limits (interface-only JDK proxies, subclass-only CGLIB). The small positive score reflects that the sharpest practical pitfalls are asserted as consequences in a truncated article rather than demonstrated, which nudges the rhetoric slightly ahead of the shown evidence.
Community explainer, no disclosed commercial stake
The only source is an individual-author post on a community developer platform about a widely used open-source framework. There is no product, vendor, sponsor, funding round or pricing under discussion, and no benchmark competing with a named rival, so the visible incentive is audience and reputation building rather than commercial positioning. Low rather than zero because single-author explainer content rewards confident, tidy narratives over caveats and citations, which is consistent with the missing documentation links and truncated pitfalls section.
Single-source but structurally verifiable
Confidence is capped by having one publisher, no corroborating documentation and a body truncated mid-sentence before the pitfalls. It is held above the floor because the assertions are about stable, publicly inspectable JDK and Spring behaviour shown in code, the derived claims follow directly from that mechanism, and nothing in the cluster contradicts them.
build
The stopping problem: an LLM rewrite loop that converged on code javac rejected1 distinct publisher
build
In Spring, the hook you need is decided by the clock, not by the name1 distinct publisher
build
Three services you can delete: queue, cache and search in one Postgres1 distinct publisher
build
The @Version field that guarded nothing: JPA counters, bulk UPDATEs and quietly missing clicks1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 16, 2026