Spring AOP (Aspect-Oriented Programming)

Architect-Level Notes on Spring AOP

What is AOP?

Aspect-Oriented Programming (AOP) allows separation of cross-cutting concerns such as:

  • Logging

  • Security

  • Transactions

  • Auditing

  • Caching

  • Monitoring

Instead of duplicating logic across classes, AOP modularizes them into reusable components called Aspects.


Why AOP?

Without AOP:

Controller → Service → Repository Each layer duplicates logging, security, transaction logic.

With AOP:

Cross-cutting concerns are applied dynamically via proxies.


AOP Core Concepts

| Term | Description | |------|------------| | Aspect | Cross-cutting logic module | | Join Point | A point in execution (method call in Spring AOP) | | Advice | Action taken at join point | | Pointcut | Expression selecting join points | | Target | Object being advised | | Proxy | Object wrapping target | | Weaving | Linking aspects with target |


Spring AOP Architecture

Spring provides a subproject, the Spring AOP, which offers a pure Java solution for defining method execution join points on the target object—the Spring beans—by employing the Proxy pattern. You can think of the proxy objects as the wrappers around the actual objects, so the features can be introduced before, after, or around the method calls of the originator objects.

Spring AOP only applies runtime weaving while creating the proxy object mentioned, so there is no need to do any processing at the compilation time of the classes.

Diagram

Spring AOP uses:

  • Proxy-based weaving (Runtime)

  • Method-level join points only

Unlike AspectJ (compile-time weaving), Spring AOP works at runtime.


Proxy Mechanism

Spring creates proxies in two ways:

  1. JDK Dynamic Proxy (Interface-based)

  2. CGLIB Proxy (Subclass-based)


Proxy Selection Flow

Diagram

JDK Proxy

  • Requires interface

  • Proxies interface methods only

CGLIB Proxy

  • Subclasses target class

  • Works without interface

  • Cannot proxy final classes/methods

Force CGLIB:

@EnableAspectJAutoProxy(proxyTargetClass = true)

Advice Types (Very Important)

Type Interface Execution Point

Before

MethodBeforeAdvice

The advice gets executed before the join-point. After Returning

AfterReturningAdvice

The advice gets executed after the execution of the join-point finishes. After Throwing

ThrowsAdvice

The advice gets executed if any exception is thrown from the join-point. After (Finally)

N/A

The advice gets executed after the execution of the join-point whether it throws an exception or not. Around

Spring supports 5 types of Advice.


1. @Before

Executes before method call.

@Before("execution(* com.bank.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
    System.out.println("Method called: " + joinPoint.getSignature());
}

Use cases:

  • Input validation

  • Logging

  • Security checks


2. @After

Runs after method finishes (success or exception).

@After("execution(* com.bank.service.*.*(..))")
public void after() {
    System.out.println("Method executed");
}

3. @AfterReturning

Runs only if method completes successfully.

@AfterReturning(
    pointcut = "execution(* com.bank.service.*.*(..))",
    returning = "result"
)
public void afterReturning(Object result) {
    System.out.println("Returned: " + result);
}

4. @AfterThrowing

Runs if method throws exception.

@AfterThrowing(
    pointcut = "execution(* com.bank.service.*.*(..))",
    throwing = "ex"
)
public void handleException(Exception ex) {
    System.out.println("Exception: " + ex.getMessage());
}

5. @Around (Most Powerful)

Controls entire method execution.

@Around("execution(* com.bank.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.currentTimeMillis();
    Object result = pjp.proceed();
    long end = System.currentTimeMillis();
    System.out.println("Execution time: " + (end - start));
    return result;
}

Use cases:

  • Performance monitoring

  • Retry logic

  • Circuit breakers

  • Transaction-like behavior


JoinPoint Details

JoinPoint provides:

  • Method name

  • Arguments

  • Target object

  • Signature

joinPoint.getSignature().getName();
joinPoint.getArgs();
joinPoint.getTarget();

Spring AOP supports only:

  • Method execution join points The return type of the method should be void and the parameters of the method should match the parameters of the point-cut expression. For example, if the point-cut expression is execution(* com.bank.service..(..)), the method should have any return type, and it can have any number of parameters of any type.


Pointcut Expressions (Deep Dive)

Syntax:

execution(modifiers-pattern? return-type-pattern declaring-type-pattern?
method-name-pattern(param-pattern) throws-pattern?)

Example patterns:

Match all service methods:

execution(* com.bank.service.*.*(..))

Match specific method:

execution(public * transfer(..))

Match annotation:

@annotation(org.springframework.transaction.annotation.Transactional)

Combine expressions:

execution(* com.bank..*(..)) && @annotation(Loggable)

Custom Annotation + AOP

Define annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable { }

Aspect:

@Around("@annotation(Loggable)")
public Object logExecution(ProceedingJoinPoint pjp) throws Throwable {
    return pjp.proceed();
}

AOP + Transactions (Important)

@Transactional works using AOP proxy internally.

Flow:

Diagram

Important Rule:

Transactional method must be called from outside the bean.

Self-invocation will NOT trigger proxy.


AOP Execution Order

Multiple aspects are ordered using:

@Order(1)

Lower value = Higher priority


Banking Perspective

Use AOP for:

  • Audit logging

  • Compliance tracking

  • Security enforcement

  • Performance monitoring

Avoid:

  • Complex business logic inside aspects

  • Long-running DB calls inside advice


FAANG Perspective

Use AOP for:

  • Distributed tracing

  • Metrics collection

  • Centralized logging

  • Rate limiting

Often combined with:

  • Micrometer

  • OpenTelemetry

  • Observability stacks


Performance Considerations

  • Proxy adds slight overhead

  • Avoid excessive pointcuts

  • Use specific expressions instead of wildcards

  • Heavy @Around advice may impact latency


Common Pitfalls

  1. Self-invocation problem

  2. Final methods not proxied (CGLIB)

  3. Private methods not advised

  4. Overusing AOP for business logic

  5. Incorrect pointcut expression causing performance hit


Spring AOP vs AspectJ

| Feature | Spring AOP | AspectJ | |----------|-------------|----------| | Weaving | Runtime | Compile-time / Load-time | | Join Points | Method only | Method, constructor, field | | Complexity | Low | High | | Power | Moderate | Very High |

Spring AOP is sufficient for most enterprise apps.


Architect-Level Summary

Spring AOP:

  • Uses dynamic proxies

  • Enables modular cross-cutting concerns

  • Powers @Transactional internally

  • Should be used for infrastructure concerns only

  • Not a replacement for proper architecture

Banking → Controlled, minimal aspects FAANG → Observability, tracing, metrics-heavy aspects

Internal Working of Spring AOP Proxy Chain

Spring AOP creates a proxy around the target bean.

When multiple aspects exist, Spring builds an interceptor chain.

Execution order:

Diagram

Each Advice becomes a MethodInterceptor internally.

Execution Stack (Around Advice example):

  1. Proxy receives method call

  2. InterceptorChain created

  3. First interceptor executes

  4. Calls proceed()

  5. Next interceptor executes

  6. Eventually target method invoked

  7. Control unwinds back through interceptors

Internally Spring uses:

  • AdvisedSupport

  • MethodInterceptor

  • ReflectiveMethodInvocation

Key Insight (Interview):

Spring AOP = Chain of Responsibility pattern over dynamic proxy.


Self Invocation Problem (Important)

Problem:

public void outer() {
    inner();  // Will NOT trigger AOP
}

@Transactional
public void inner() { }

Why?

Call does not go through proxy.

Solution:

  • Inject self bean

  • Or refactor to another service

Architect Tip:

Keep transactional boundaries at service entry level.


Load-Time Weaving (LTW)

Spring AOP is proxy-based (runtime).

AspectJ supports:

  • Compile-time weaving

  • Load-time weaving (LTW)

LTW modifies bytecode during class loading.

Architecture:

Diagram

Enable LTW:

@EnableLoadTimeWeaving

Requires:

  • AspectJ weaver agent

  • JVM argument: -javaagent:aspectjweaver.jar

When to use LTW:

  • Need field-level interception

  • Need constructor interception

  • Need domain model weaving

Banking systems rarely require LTW. Large distributed observability platforms may.


OpenTelemetry + AOP Example

Goal:

Automatically trace method execution.

Custom annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Traceable { }

Aspect:

@Aspect
@Component
public class TracingAspect {

    private final Tracer tracer;

    public TracingAspect(Tracer tracer) {
        this.tracer = tracer;
    }

    @Around("@annotation(Traceable)")
    public Object trace(ProceedingJoinPoint pjp) throws Throwable {

        Span span = tracer.spanBuilder(pjp.getSignature().getName())
                .startSpan();

        try (Scope scope = span.makeCurrent()) {
            return pjp.proceed();
        } catch (Exception ex) {
            span.recordException(ex);
            throw ex;
        } finally {
            span.end();
        }
    }
}

This integrates with:

  • OpenTelemetry

  • Jaeger

  • Zipkin

  • Grafana Tempo

Architect Insight:

In FAANG-style systems, observability is non-negotiable.


Distributed Tracing Architecture

Diagram

Trace Context Propagation:

  • traceId

  • spanId

  • parentSpanId

Headers used:

  • traceparent

  • b3

Spring Boot auto-configures this via:

  • Micrometer Tracing

  • OpenTelemetry SDK


Retry with AOP

Simple Retry Aspect:

@Around("@annotation(Retryable)")
public Object retry(ProceedingJoinPoint pjp) throws Throwable {

    int attempts = 3;
    while (attempts > 0) {
        try {
            return pjp.proceed();
        } catch (Exception ex) {
            attempts--;
            if (attempts == 0) throw ex;
        }
    }
    return null;
}

Better Production Option:

Use Spring Retry:

@Retryable(value = RuntimeException.class, maxAttempts = 3)
public void callExternalService() { }

Circuit Breaker with AOP (Simplified)

Concept:

Prevent cascading failures.

States:

  • CLOSED

  • OPEN

  • HALF_OPEN

Architecture:

Diagram

Basic Circuit Breaker Aspect Concept:

@Around("@annotation(CircuitProtected)")
public Object circuit(ProceedingJoinPoint pjp) throws Throwable {

    if (state == OPEN) {
        throw new RuntimeException("Circuit Open");
    }

    try {
        Object result = pjp.proceed();
        resetFailures();
        return result;
    } catch (Exception ex) {
        recordFailure();
        throw ex;
    }
}

Production-ready approach:

Use Resilience4j.


AOP in Microservices Architecture

Common cross-cutting implementations:

  • Logging

  • Metrics

  • Rate limiting

  • Security

  • Tracing

  • Retry

  • Circuit breaking

Architecture:

Diagram

Performance Impact Discussion (Interview Ready)

Spring AOP overhead:

  • One proxy invocation

  • Interceptor chain

  • Reflection call

Impact:

  • Minimal for business apps

  • Significant if overused in hot paths

Best Practice:

  • Use targeted pointcuts

  • Avoid broad wildcard expressions

  • Avoid heavy logic inside @Around


When NOT to Use AOP

Avoid for:

  • Core business logic

  • Complex domain rules

  • Performance-critical tight loops

  • Cross-service orchestration

Use AOP for:

Infrastructure concerns only.


Architect-Level Summary

Spring AOP:

  • Proxy-based runtime weaving

  • Chain of interceptors

  • Powers @Transactional

  • Enables observability

  • Supports resilience patterns

  • Must be used carefully in high-scale systems

Banking: - Minimal aspects - Audit + transactions only

FAANG: - Heavy observability - Distributed tracing - Metrics + resilience patterns - Event-driven instrumentation