// Interview Preparation

Spring AOP
Deep Dive

Production-grade answers for senior Java interviews. Every question backed by real exception scenarios and architectural reasoning.

Proxy Pattern JDK Dynamic Proxy CGLIB AspectJ @Transactional Traps Pointcut DSL
01

Proxy Internals — JDK vs CGLIB

Q

How does Spring decide between JDK Dynamic Proxy and CGLIB?

Mid

Rule: If the bean's class implements at least one interface, Spring uses JDK Dynamic Proxy by default. If there's no interface, or if proxyTargetClass=true is set, Spring falls back to CGLIB.

// JDK Proxy — requires interface
public interface PaymentService {
    void process(String txId);
}

@Service
public class PaymentServiceImpl implements PaymentService {
    public void process(String txId) { ... }
}
// Spring wraps this in a JDK proxy implementing PaymentService
// InvocationHandler intercepts each method call
// CGLIB — subclasses the concrete class
@Service
public class ReportService {     // No interface
    public void generate() { ... }
}
// Spring creates: ReportService$$EnhancerBySpringCGLIB$$abc123
// Cannot be final class or final methods — CGLIB needs to subclass!
AspectJDK Dynamic ProxyCGLIB
MechanismImplements same interface via reflectionSubclasses the target class at runtime
RequiresAt least one interfaceNon-final class, non-final methods
SpeedSlower (reflection per call)Faster (bytecode generation, cached)
Default in Spring BootNo — CGLIB is default since Spring Boot 2.xYes, spring.aop.proxy-target-class=true
⚠ Common Mistake Spring Boot 2.x changed the default to CGLIB for @Configuration and @Transactional. Autowiring by concrete type will now work, but final classes will break. If you declare class FooService final, CGLIB will throw at startup.
PROD

Production: BeanCreationException — Cannot subclass final class. How do you debug this?

Hard
Exception org.springframework.beans.factory.BeanCreationException: Error creating bean 'paymentService': CGLIB subclassing failed for ... Cannot subclass final class

Root cause: CGLIB is the default proxy strategy in Spring Boot 2+. If your service class is declared final, CGLIB cannot create a subclass — startup fails.

  • Remove final from the class or use an interface + switch to JDK proxy
  • Add spring.aop.proxy-target-class=false in application.properties to force JDK proxy
  • If using Kotlin — Kotlin classes are final by default! Add kotlin-allopen plugin or annotate with open
// Kotlin fix — use allopen plugin in build.gradle
// build.gradle.kts
plugins {
    kotlin("plugin.allopen") version "1.9.0"
}
allOpen {
    annotation("org.springframework.stereotype.Service")
    annotation("org.springframework.transaction.annotation.Transactional")
}
Q

What does InvocationHandler do internally in JDK proxy? Walk me through a method call.

Hard

When Spring creates a JDK Dynamic Proxy, it calls Proxy.newProxyInstance() with a custom InvocationHandler. Every method call on the proxy goes through handler.invoke(proxy, method, args).

// Conceptual implementation of what Spring does internally
class SpringAopInvocationHandler implements InvocationHandler {
    private final Object target;
    private final List<MethodInterceptor> interceptors;

    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        // 1. Build interceptor chain (aspects in order)
        MethodInvocation invocation =
            new ReflectiveMethodInvocation(target, method, args, interceptors);
        // 2. Proceed through chain — each aspect can wrap/intercept
        return invocation.proceed();  // eventually calls target.method(args)
    }
}

// Usage: proxy is wired into beans, target is the real service
PaymentService proxy = (PaymentService) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    new Class[]{PaymentService.class},
    new SpringAopInvocationHandler(target, interceptors)
);

The chain: Caller → Proxy → Interceptor 1 (Logging) → Interceptor 2 (Transaction) → Real Method. This is the Chain of Responsibility pattern.

02

Pointcut Expressions — Full Breakdown

Q

Break down a complex pointcut expression token by token.

Mid
// Pointcut anatomy
@Pointcut("execution(* com.myapp.service.*.*(..)) && !execution(* com.myapp.service.*Repo.*(..))")

/*
  execution(                     → match method execution join points
    *                            → any return type
    com.myapp.service.           → package
    *                            → any class in that package
    .                            → separator
    *                            → any method name
    (..)                         → any args (0 or more, any type)
  )
  && !execution(...)             → AND NOT — exclude Repo classes
*/
TokenMeaningExample
*Any single element (type, method name, one arg)*.service.*
..Any number of things (packages or args)com.app.. or (..)
+Type plus all subtypesBaseService+
&&, ||, !Logical compositionA() && !B()
within()Match all methods in a type/packagewithin(com.app.service.*)
@annotation()Match methods with annotation@annotation(Audited)
args()Match by arg typeargs(String, ..)
bean()Spring-specific: match bean namebean(*Service)
// Real-world examples
// All public methods in service layer
"execution(public * com.app.service..*(..))"

// Methods returning void
"execution(void com.app.service.*.*(..))"

// Methods with @Transactional (Spring-managed classes only)
"@annotation(org.springframework.transaction.annotation.Transactional)"

// Any method that throws an exception type — use @AfterThrowing for this
"execution(* com.app..*(..))"

// Methods in classes annotated with @RestController
"@within(org.springframework.web.bind.annotation.RestController)"
PROD

Production: Aspect fires on unwanted beans — causing performance issues in prod. How did you track it?

Hard

This is a very common production issue. A timing aspect with a broad pointcut like execution(* *.*(..)) accidentally intercepts Spring infrastructure beans, repository calls, and even Hibernate internal methods — causing enormous overhead.

Symptom Application was 3x slower in prod than local. Thread dump showed massive time in AOP interceptors. Logs flooded with timing entries for internal Spring classes.

Debugging steps:

  • Enable logging.level.org.springframework.aop=DEBUG to see which proxies are created
  • Use -Dspring.aop.proxy-target-class=true and check proxy chain depth
  • Add a within() scope constraint to narrow down the pointcut
// WRONG — too broad
@Pointcut("execution(* *.*(..))")  // catches EVERYTHING including Spring internals
// CORRECT — scoped to your packages only
@Pointcut("execution(* com.mycompany.app..*(..)) "
    + "&& !within(com.mycompany.app.config..*)"
    + "&& !within(com.mycompany.app.aspect..*)")  // exclude aspects themselves!
public void applicationLayer() {}
🔑 Key Insight Always use package-scoped pointcuts. Never use bare execution(* *.*(..)) in any environment. Aspects intercepting other aspects cause infinite loops.
03

@Around vs @Before vs @After — When to Use What

Q

Explain all advice types with execution order and real use cases.

Mid
// Execution order: @Around wraps everything
// Call sequence for a single method execution:
// 1. @Around (before proceed)
// 2. @Before
// 3. TARGET METHOD EXECUTES
// 4. @AfterReturning (if success) OR @AfterThrowing (if exception)
// 5. @After (always — like finally)
// 6. @Around (after proceed, return value)
AdviceWhenCan change return?Use Case
@BeforeBefore method callNoValidation, auth check, param logging
@AfterReturningAfter successNo (read only)Audit logging return value
@AfterThrowingAfter exceptionNo (read exception)Error reporting, alerting
@AfterAlways (finally)NoResource cleanup
@AroundWraps everythingYesTiming, caching, retry, circuit breaker
// @Around — full control, must call pjp.proceed()
@Around("execution(* com.app.service.*.*(..))")
public Object timingAndRetry(ProceedingJoinPoint pjp) throws Throwable {
    int maxRetries = 3;
    for (int i = 0; i < maxRetries; i++) {
        try {
            long start = System.currentTimeMillis();
            Object result = pjp.proceed();   // MUST be called or method never runs!
            long elapsed = System.currentTimeMillis() - start;
            log.info("{} took {}ms", pjp.getSignature(), elapsed);
            return result;
        } catch (TransientException e) {
            if (i == maxRetries - 1) throw e;
        }
    }
    throw new RuntimeException("Should never reach here");
}
Critical Bug In @Around, if you forget to call pjp.proceed(), the actual method NEVER executes. Your service returns null silently. No exception, no log — just null. This is a silent data loss bug in production.
PROD

Production: @AfterThrowing not catching RuntimeException — aspect silently fails

Hard

A common trap: @AfterThrowing will only fire if the exception propagates out of the proxied method. If the method catches it internally and swallows it, the aspect never sees it.

// @AfterThrowing won't fire here — exception is swallowed
public void processPayment() {
    try {
        gateway.charge();
    } catch (Exception e) {
        log.error("Payment failed", e);  // swallowed! Aspect never sees this
    }
}

@AfterThrowing(pointcut = "execution(* com.app.service.*.*(..))",
               throwing = "ex")
public void alertOnException(Exception ex) {
    // This NEVER fires because exception was caught above
    alertingService.notify(ex);
}
Fix Use @Around with try-catch instead if you need to observe ALL exceptions regardless of internal handling. Or ensure the service re-throws after logging.
04

Self-Invocation Bug — The #1 Production AOP Pitfall

PROD

Explain the self-invocation proxy bypass problem and all 3 ways to fix it.

Hard

This is the most asked AOP question in senior Java interviews — and the most common production bug.

The Problem: Spring AOP works by wrapping beans in proxies. When code OUTSIDE the bean calls a method, it goes through the proxy (aspects fire). But when a method inside the bean calls another method of the same bean using this, it bypasses the proxy entirely — no aspects, no @Transactional.

// The Bug — classic self-invocation
@Service
public class OrderService {

    public void placeOrder(Order order) {
        // Called from controller → goes through proxy ✓
        saveOrder(order);    // ← this.saveOrder() — BYPASSES PROXY ✗
                              // @Transactional on saveOrder does NOTHING
    }

    @Transactional
    public void saveOrder(Order order) {
        // Called from placeOrder() via 'this' — not the proxy
        // No transaction context! Any DB write here can fail silently
        repo.save(order);
    }
}
Production Impact Data written without a transaction. On DB failure, no rollback occurs. Records partially saved. No exception in logs because Hibernate session was bound but no TX boundary existed.

Fix 1: Inject self (ApplicationContext)

// Fix 1 — Self injection via @Autowired (Spring 4.3+)
@Service
public class OrderService {

    @Autowired
    private OrderService self;  // Spring injects the PROXY, not 'this'

    public void placeOrder(Order order) {
        self.saveOrder(order);   // ← goes through proxy ✓ TX active!
    }

    @Transactional
    public void saveOrder(Order order) {
        repo.save(order);         // Now has proper transaction boundary
    }
}

Fix 2: AopContext.currentProxy()

// Fix 2 — AopContext (requires exposeProxy=true)
@EnableAspectJAutoProxy(exposeProxy = true)  // needed in @Configuration
public class AppConfig {}

@Service
public class OrderService {
    public void placeOrder(Order order) {
        ((OrderService) AopContext.currentProxy()).saveOrder(order);
    }

    @Transactional
    public void saveOrder(Order order) { repo.save(order); }
}

Fix 3: Refactor to separate bean (Recommended)

// Fix 3 — Best practice: separate concerns into different beans
@Service
public class OrderService {
    @Autowired
    private OrderPersistenceService persistSvc;

    public void placeOrder(Order order) {
        persistSvc.saveOrder(order);  // external call → proxy → TX fires ✓
    }
}

@Service
public class OrderPersistenceService {
    @Transactional
    public void saveOrder(Order order) { repo.save(order); }
}
Interview Answer Fix 3 is always the best answer. Fix 1 creates circular dependency risk. Fix 2 tightly couples to Spring AOP internals. Separation of concerns is the clean solution.
05

@Transactional — Traps & Production Scenarios

PROD

Why does @Transactional on a private method not work? What exception do you get (or not get)?

Hard

Spring AOP proxies can only intercept public methods. A @Transactional on a private, protected, or package-private method is silently ignored — no exception, no warning by default.

// Silent failure — no error, no transaction
@Service
public class UserService {

    @Transactional              // ← IGNORED. Spring AOP can't proxy private methods
    private void createUserInternal(User u) {
        userRepo.save(u);      // No rollback on exception. Data corruption risk.
        auditRepo.log(u);
    }

    public void createUser(User u) {
        createUserInternal(u);   // This is just a plain method call
    }
}
Production Bug userRepo.save() succeeds, then auditRepo.log() throws — but no rollback. User record exists in DB without audit trail. Data integrity violation discovered 3 days later during audit.

Enable this warning explicitly:

// Enable warning in Spring Boot
# application.properties
spring.jpa.open-in-view=false
logging.level.org.springframework.transaction=DEBUG
# Use AspectJ compile-time weaving to support private method TX
# (rarely justified — refactor is always better)
PROD

@Transactional rollback doesn't happen on checked exceptions — why?

Hard

By default, Spring only rolls back on unchecked exceptions (RuntimeException and Error). Checked exceptions are treated as "expected" failures — transaction commits!

// Bug — checked exception does NOT trigger rollback
@Transactional
public void transferFunds(long fromId, long toId, BigDecimal amount)
        throws InsufficientFundsException {  // ← checked
    debit(fromId, amount);  // DB write succeeds
    if (balance < 0) {
        throw new InsufficientFundsException("Not enough funds");
        // TX COMMITS! debit already happened. Money gone.
    }
    credit(toId, amount);
}
// Fix — explicitly set rollbackFor
@Transactional(rollbackFor = Exception.class)
public void transferFunds(...) throws InsufficientFundsException {
    // Now checked exceptions also trigger rollback
}

// Or be specific:
@Transactional(rollbackFor = {InsufficientFundsException.class, PaymentException.class})
public void transferFunds(...) { ... }
Fintech Interview Note In payment/banking systems, always use rollbackFor = Exception.class or convert all domain exceptions to unchecked. Partial transaction commits are financial data corruption.
ADV

Explain @Transactional propagation levels — REQUIRES_NEW vs NESTED with a real scenario.

Hard
PropagationBehaviorUse Case
REQUIREDJoin existing TX or create newDefault — most operations
REQUIRES_NEWAlways new TX; suspend outerAudit logs, independent operations
NESTEDSavepoint within outer TXPartial rollback within same TX
MANDATORYMust have TX; exception if noneInternal-only methods
NEVERMust NOT have TX; exception if one existsRead-only reporting
SUPPORTSJoin TX if exists, else run withoutOptional TX context
NOT_SUPPORTEDSuspend TX if exists, run withoutNon-TX operations in TX context
// REQUIRES_NEW — audit must succeed even if order fails
@Service
public class OrderService {

    @Autowired private AuditService auditService;

    @Transactional  // TX-1
    public void placeOrder(Order order) {
        orderRepo.save(order);
        auditService.log(order);  // runs in separate TX-2
        throw new RuntimeException("Order failed");
        // TX-1 rolls back — order not saved
        // TX-2 already committed — audit IS saved ✓
    }
}

@Service
public class AuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(Order order) {
        auditRepo.save(new AuditEntry(order));  // always persisted
    }
}
// NESTED — savepoint: retry item without losing entire batch
@Transactional  // Outer TX
public void processBatch(List<Item> items) {
    for (Item item : items) {
        try {
            itemProcessor.processItem(item);  // NESTED TX
        } catch (Exception e) {
            // NESTED rolls back to savepoint only
            // Outer TX continues — rest of batch still processes
            log.warn("Skipping item {}", item.getId());
        }
    }
}

@Transactional(propagation = Propagation.NESTED)
public void processItem(Item item) {
    itemRepo.save(item);  // savepoint created before this
}
06

Exception Scenarios — What Interviewers Actually Ask

PROD

LazyInitializationException in production — root cause and 3 solutions

Hard
Exception org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: User.orders — could not initialize proxy — no Session

Root cause: The Hibernate session closes when the @Transactional method returns. Accessing lazy-loaded collections in the controller/view layer (after TX ends) triggers this.

// Root cause scenario
@Transactional
public User getUser(long id) {
    return userRepo.findById(id).get();  // session closes after return
}

// In controller — session is GONE
User user = userService.getUser(1L);
user.getOrders().size();  // LazyInitializationException!

Solution 1: Eager fetch in query (best for specific use cases)

@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id")
User findByIdWithOrders(@Param("id") Long id);

Solution 2: Use DTOs — never expose entities to the view layer

@Transactional(readOnly = true)
public UserDTO getUser(long id) {
    User u = userRepo.findById(id).get();
    return new UserDTO(u.getId(), u.getOrders());  // accessed within TX
}

Solution 3: @Transactional(readOnly=true) on controller (anti-pattern — avoid)

// Never use open-session-in-view = true in prod (lazy loads on serialization — N+1)
spring.jpa.open-in-view=false  // Always set this in application.properties
PROD

UnexpectedRollbackException in microservices — service B rolled back service A's TX

Hard
Exception org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

Root cause: Inner @Transactional (REQUIRED) method catches an exception and rethrows it. Spring marks the TX as rollback-only. The outer caller catches it and tries to commit — but TX is already poisoned.

// The trap
@Transactional  // Outer — TX-1
public void outerMethod() {
    try {
        innerService.innerMethod();  // throws → TX-1 marked rollback-only
    } catch (Exception e) {
        // we handle it — but TX is already poisoned
        log.warn("Handled", e);
    }
    orderRepo.save(order);  // TX-1 commit attempted
    // → UnexpectedRollbackException! TX was rollback-only
}

@Transactional  // REQUIRED — joins TX-1, not a new TX
public void innerMethod() {
    throw new RuntimeException("Inner failed");  // marks TX-1 rollback-only
}
// Fix — use REQUIRES_NEW to isolate the inner TX
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerMethod() {
    // Separate TX-2 — if this rolls back, TX-1 is unaffected
}
07

Aspect Ordering — @Order & Ordered Interface

ADV

When multiple aspects apply to the same method, what determines order? How do you control it?

Hard

Without explicit ordering, aspect execution order is undefined — it depends on class loading order which varies per JVM run. In production this means non-deterministic behavior: sometimes Security runs before Logging, sometimes after.

// Control order with @Order — lower value = higher priority (outer wrapper)
@Aspect
@Component
@Order(1)  // runs first (outermost) — wraps everything inside
public class SecurityAspect {
    @Before("execution(* com.app.service.*.*(..))")
    public void checkAuth() { ... }
}

@Aspect
@Component
@Order(2)
public class LoggingAspect {
    @Before("execution(* com.app.service.*.*(..))")
    public void log() { ... }
}

@Aspect
@Component
@Order(3)
public class TransactionAspect {
    // @Transactional = @Order(Integer.MAX_VALUE - 2) by default
    // Security → Logging → TX → Method → TX → Logging → Security
}
Execution Order Model Think of it as nested boxes: @Order(1) is the outermost box. For @Before, lower order fires first. For @After/@AfterReturning, lower order fires last (because it's the outermost wrapper). @Transactional has a default order of Integer.MAX_VALUE — meaning it's innermost by default.
08

JoinPoint vs ProceedingJoinPoint

Q

What information can you extract from a JoinPoint? Walk through a real logging scenario.

Mid
// Full JoinPoint API usage in production logging
@Aspect
@Component
public class AuditAspect {

    @AfterReturning(
        pointcut = "@annotation(com.app.annotation.Auditable)",
        returning = "returnValue")
    public void audit(JoinPoint jp, Object returnValue) {

        MethodSignature sig = (MethodSignature) jp.getSignature();

        String className  = jp.getTarget().getClass().getSimpleName();
        String methodName = sig.getName();
        String[] paramNames = sig.getParameterNames();
        Object[] args      = jp.getArgs();

        // Build audit log with arg name→value pairs
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < paramNames.length; i++) {
            sb.append(paramNames[i]).append("=").append(args[i]).append(",");
        }

        auditLog.info("[{}#{}] args=[{}] return=[{}]",
            className, methodName, sb, returnValue);

        // jp.getThis()        → proxy object reference
        // jp.getTarget()      → actual target bean
        // jp.getArgs()        → method arguments array
        // jp.getSignature()   → MethodSignature with full type info
        // jp.getKind()        → "method-execution"
    }
}
ProceedingJoinPoint Only available in @Around advice. Adds proceed() and proceed(Object[]) methods. The proceed(Object[]) variant lets you replace method arguments — useful for input sanitization aspects.
09

Custom Annotation Aspects — Production Patterns

ADV

Design a @RateLimit annotation backed by an AOP aspect and Redis. Show full implementation.

Hard

This is a senior-level design question that tests your ability to combine AOP with real infrastructure. Rate limiting via AOP is used in API gateway services, fintech platforms, and any high-traffic microservice.

// Step 1: Define the annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int requests() default 100;          // max requests
    int windowSeconds() default 60;      // per time window
    String keyPrefix() default "rl";     // Redis key prefix
}
// Step 2: Aspect implementation with Redis
@Aspect
@Component
@Order(1)  // highest priority — check rate before anything
public class RateLimitAspect {

    @Autowired
    private StringRedisTemplate redis;

    @Around("@annotation(rateLimit)")
    public Object enforce(ProceedingJoinPoint pjp,
                          RateLimit rateLimit) throws Throwable {

        String userId = SecurityContextHolder.getContext()
                            .getAuthentication().getName();

        String method = pjp.getSignature().getName();
        String redisKey = String.format("%s:%s:%s",
            rateLimit.keyPrefix(), userId, method);

        Long current = redis.opsForValue().increment(redisKey);
        if (current == 1) {  // first request — set expiry
            redis.expire(redisKey, rateLimit.windowSeconds(), TimeUnit.SECONDS);
        }
        if (current > rateLimit.requests()) {
            throw new TooManyRequestsException(
                "Rate limit exceeded for user: " + userId);
        }
        return pjp.proceed();
    }
}

// Usage
@RateLimit(requests = 10, windowSeconds = 60)
@PostMapping("/transfer")
public ResponseEntity<?> transferFunds(@RequestBody TransferRequest req) { ... }
10

Weaving Types — Compile-time vs Load-time vs Runtime

Q

Compare weaving types and when would you choose AspectJ over Spring AOP?

Mid
TypeWhenHowSupports
Compile-timeAt javac timeAspectJ compiler (ajc)Private, static, constructor
Load-time (LTW)Class loadingJava agent + AspectJ weaverPrivate, static, constructor
RuntimeStartupSpring AOP proxiesPublic methods on Spring beans only

Choose AspectJ (compile/LTW) when:

  • You need to advise private methods (e.g., domain model methods in DDD)
  • You need to intercept new constructors or static methods
  • Maximum performance needed — no proxy overhead
  • You want to apply aspects to non-Spring-managed objects

Choose Spring AOP (runtime) when:

  • You only need to advise public Spring bean methods (95% of cases)
  • Simpler setup, no special compiler or JVM agent needed
  • Your team is comfortable with proxy limitations
// Enable LTW in Spring Boot (if you must use AspectJ)
// application.properties
spring.aop.auto=false  // disable Spring AOP proxies

// JVM startup flag
-javaagent:/path/to/aspectjweaver.jar

// META-INF/aop.xml — define aspects for LTW
<aspectj>
    <weaver options="-verbose">
        <include within="com.myapp..*"/>
    </weaver>
</aspectj>
11

Rapid Fire — Real Interview Q&A

Q

Can AOP be applied to Spring Data JPA repositories?

Mid

Yes — Spring Data JPA repositories are Spring beans so Spring AOP proxies apply. However, Spring wraps them in its own proxy chain already (for transactions, query execution), so you get a double-proxy. Use within(org.springframework.data.repository.Repository+) pointcut but be aware of performance impact. Usually better to add an intermediate service layer and put aspects there.

Q

What happens if an aspect itself throws an exception?

Mid

In @Before — the target method never executes. The exception propagates to the caller as-is. In @Around — if the exception is thrown before pjp.proceed(), same effect. If after, the return value from the method is discarded. In @AfterReturning/@After — exception propagates but the method has already returned. If the method returned a value, it's lost.

Key Rule An aspect should NEVER throw checked exceptions that are not declared on the target method's signature. Spring will wrap them in UndeclaredThrowableException — a confusing runtime exception with no meaningful stack trace for the caller.
Q

How do you test aspects in isolation without Spring context?

Mid
// Unit test aspect directly using AspectJ proxy factory
@Test
void testTimingAspectFires() {
    TimingAspect aspect = new TimingAspect();
    MyService target = new MyServiceImpl();

    AspectJProxyFactory factory = new AspectJProxyFactory(target);
    factory.addAspect(aspect);
    MyService proxy = factory.getProxy();

    proxy.doWork();  // aspect fires on this call
    // verify aspect behavior: timing logged, etc.
    Mockito.verify(aspect.getLogger()).info(Mockito.contains("ms"));
}
PROD

StackOverflowError caused by an aspect — how and why?

Hard
Exception java.lang.StackOverflowError at ... TimingAspect.time(TimingAspect.java:12)

Two causes: (1) Aspect pointcut matches the aspect itself — the aspect advises its own methods, creating infinite recursion. Fix: add && !within(com.app.aspect..*) to exclude aspect package. (2) Aspect calls a Spring bean method that is also matched by the same pointcut — the aspect triggers itself through the proxy chain.

// Cause 1 fix
@Pointcut("execution(* com.app..*(..)) && !within(com.app.aspect..*)")
public void appMethods() {}

// Cause 2 — aspect calls service that matches its own pointcut
// Fix: use a separate logger/service that is outside the pointcut scope
// or inject via @Autowired and check isProxy before calling
ADV

What is the difference between @DeclareParents (Introduction) and regular advice?

Hard

Introduction (Mixin) allows you to add new interfaces and their implementations to existing beans at runtime — without modifying the class. Useful in legacy codebases where you can't change the original class.

// Add Auditable interface to all service beans without touching them
public interface Auditable {
    String getLastModifiedBy();
    void setLastModifiedBy(String user);
}

public class DefaultAuditable implements Auditable {
    private String lastModifiedBy;
    // getters/setters
}

@Aspect
@Component
public class AuditIntroductionAspect {
    @DeclareParents(
        value = "com.app.service.*+",          // all service beans
        defaultImpl = DefaultAuditable.class  // mixin impl
    )
    private Auditable auditable;
}

// Now you can cast any service bean to Auditable:
PaymentService svc = context.getBean(PaymentService.class);
((Auditable) svc).setLastModifiedBy("admin");  // works via proxy!