skip to content

When would you choose Load-Time Weaving over Spring's proxy-based AOP, and what are the costs of that choice?

level: seniorimportance: should knowfreq 45%

answer

  1. proxy reach = external calls only
  2. LTW wins: self-invocation, private/final, new-objects, @Configurable
  3. cost: agent in every environment
  4. cost: startup scan + classloader fragility
  5. first try: refactor into a collaborator bean

basics

~10 s

Choose LTW when proxies can't reach the join point: self-invocations, private/final methods, non-Spring objects, or objects created with new (e.g. @Configurable). Costs are a required Java agent, slower startup, and classloader/deployment complexity.

solid answer

~40 s

Proxies only intercept external calls through a Spring-managed wrapper, so they miss self-invocation, private/final/static methods, and any object Spring doesn't create. LTW rewrites the real bytecode, so it advises all of those, plus domain objects instantiated with new via @Configurable, and can weave library classes. The trade-off: you must ship and configure a -javaagent, which complicates every launch environment (IDE, tests, containers, CI); startup gets slower because a ClassFileTransformer inspects classes as they load; behavior becomes classloader-sensitive and harder to debug; and full AspectJ pointcut semantics differ subtly from Spring AOP. So the rule is: stay on proxies by default; adopt LTW only for a specific, proven need. Often a proxy limitation can be worked around (extract the self-called method into a collaborator bean) more cheaply than adopting LTW.

go deeper

for a junior

Know LTW handles self-invocation where proxies don't; details optional.

for a middle

List the join points LTW reaches that proxies miss and name the agent cost.

for a senior

Give a balanced trade-off: specific LTW motivations vs operational tax, plus the refactor alternative.

for a principal

Frame it as an architecture/ops decision — agent distribution across environments, startup SLAs, AspectJ semantic risk — and default the org to proxies.

## The decision hinges on join-point reach Proxy-based Spring AOP intercepts a method **only when the call passes through the proxy object** Spring created. That single fact produces all its limitations: | Scenario | Proxy AOP | LTW | |---|---|---| | Public method of a bean, called from another bean | ✅ advised | ✅ advised | | **Self-invocation** (`this.foo()` inside the bean) | ❌ bypasses proxy | ✅ advised | | **private / final / static** method | ❌ (JDK needs interface; CGLIB can't override final/private) | ✅ (bytecode rewrite) | | Object created with **`new`** (not a Spring bean) | ❌ Spring never wrapped it | ✅ via `@Configurable`/LTW | | **Library / JDK class** | ❌ | ✅ (if included in aop.xml) | ## Concrete reasons to pick LTW 1. **`@Configurable` domain objects** — you want dependency injection and advice on entities you create with `new` (classic DDD rich-domain pattern). This *requires* weaving; proxies can't help because Spring never owns the instance. 2. **Self-invocation must be advised** and you cannot restructure the code to route through a bean boundary. 3. **Non-public method advice** is genuinely needed. 4. **Uniform weaving across non-Spring code**, e.g. instrumenting third-party classes. ## The costs (why it's not the default) - **Agent everywhere**: `-javaagent:spring-instrument.jar` (or `aspectjweaver.jar`) must be present in *every* runtime — production launch, Docker image, CI, local IDE run configs, test harness. A missing agent means aspects silently don't fire, which is a nasty, environment-specific bug. - **Startup cost**: a `ClassFileTransformer` runs for classes as they load; without tight `<include>` filters this scans huge numbers of classes. Even with filters there's overhead. - **Classloader sensitivity**: weaving interacts with classloader hierarchies (app servers, containers, hot reload). Classes loaded before the weaver is installed are never woven; ordering bugs are subtle. - **Different semantics**: LTW uses the **full AspectJ** language/pointcut model, which is more powerful but differs from Spring AOP's subset — `call()` vs `execution()`, field-set pointcuts, etc. Existing `@Aspect`s may behave differently. - **Tooling/debuggability**: stack traces and debugging are murkier once bytecode is rewritten; you rely on `-showWeaveInfo` to see what happened. ## Cheaper alternatives to reach for first - **Refactor self-invocation**: move the self-called method into a separate injected bean so the call crosses a proxy boundary. Solves the most common LTW motivation without any agent. - **Self-injection / `AopContext.currentProxy()`**: get the proxy reference for the internal call (with `exposeProxy = true`). - Accept the proxy limitation if the advice (e.g. logging) isn't critical on internal calls. ## Bottom line for an interview Default to proxies. Justify LTW by naming a *specific* join point proxies can't reach (usually `@Configurable` or unavoidable self-invocation), then acknowledge the operational tax: agent distribution, startup latency, classloader fragility, and AspectJ semantic differences. A strong candidate mentions that the self-invocation case is often better solved by a small refactor than by adopting LTW wholesale.

  • A team wants LTW purely so internal this.retry() calls get their @Retryable-style advice. Is that the right call?
    Usually not. Extracting retry() into a separate injected bean makes the call cross the proxy boundary and fixes it with zero agent overhead. Adopting LTW for a single self-invocation adds an agent to every environment and startup cost — disproportionate. Reserve LTW for cases with no reasonable refactor, like @Configurable domain objects.
  • Name a semantic difference between full AspectJ (LTW) and Spring AOP pointcuts.
    Spring AOP only supports method-execution join points on Spring beans (execution()). Full AspectJ additionally supports call() join points, constructor/field-access join points, and weaving of non-Spring and non-public members — a much larger pointcut vocabulary.

saying these in an interview costs you the question

  • Recommending LTW as a general upgrade over proxies
  • Not mentioning the agent must exist in every runtime environment
  • Claiming LTW has no startup/performance cost
  • Missing that a self-invocation is often fixable by refactoring instead

context