Definition
- AOP – a programming paradigm that separates cross-cutting concerns (logging, timing, security, transactions) from business logic so they are defined once and applied automatically.
- Aspect – a class annotated with
@Aspectthat contains the cross-cutting logic. - JoinPoint – a specific point in the execution where an aspect can be applied, typically a method call.
- Pointcut – a predicate that selects which JoinPoints the advice applies to, written as a pattern matching method signatures or annotations.
- Advice – the actual code that runs at a JoinPoint:
@Before,@After,@Around,@AfterReturning,@AfterThrowing. - Proxy – a wrapper object that Spring creates around your bean. All external calls go through the proxy, which runs the advice before delegating to the real method.
Think of a CCTV camera at a National Bank of India teller window. The camera (Aspect) sits between the customer and the teller (Proxy), automatically recording the start time, end time, and outcome of every transaction (Advice) without the teller writing anything extra. The camera watches all teller windows (Pointcut), and each individual transaction is a JoinPoint.
Understanding the Concepts
What is AOP and why does it exist?
AOP stands for Aspect-Oriented Programming. The whole idea is to pull cross-cutting concerns out of your business code. A cross-cutting concern is something that almost every method needs but that has nothing to do with the actual business logic. Examples include measuring how long a method took, writing an audit entry, checking security permissions, and managing database transactions.
Without AOP, Rajesh would copy the same timing code into two hundred methods at National Bank of India. With AOP, he writes that timing logic once in an Aspect, and Spring applies it to all those methods automatically.
// WITHOUT AOP: timing code repeated in every method
package com.bank.service;
import java.math.BigDecimal;
public class TransferService {
public void transfer(String from, String to, BigDecimal amount) {
long start = System.currentTimeMillis(); // timing boilerplate
try {
// actual business logic
debit(from, amount);
credit(to, amount);
auditLog(from, to, amount);
} finally {
long duration = System.currentTimeMillis() - start;
log.info("transfer took {} ms", duration); // timing boilerplate
}
}
// Same timing boilerplate in calculateInterest(), processLoan(), etc.
}
// WITH AOP: timing logic written once, applied everywhere
// TransferService contains ONLY business logic
// TimingAspect handles timing for all methods annotated with @LogExecutionTime
How does AOP work via proxies?
When Spring sees that a bean needs an aspect applied, it does not hand your code the real object. Instead, it wraps the real object inside a proxy object that looks identical from the outside. Every call from outside the object goes to the proxy first. The proxy runs the aspect logic, then delegates to the real method, then runs any after logic, and returns the result.
There are two kinds of proxy:
| Proxy Type | When Used | Mechanism |
|---|---|---|
| JDK Dynamic Proxy | Bean implements an interface | Creates a runtime class implementing the same interface |
| CGLIB Proxy | Bean has no interface | Generates a subclass of your class at runtime |
Spring Boot defaults to CGLIB proxies for most beans.
package com.bank.service;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
@Service
public class InterestService {
// When AOP is active, Spring does NOT give you this object directly.
// It gives you a CGLIB proxy that wraps this object.
@LogExecutionTime // custom annotation, aspect intercepts this
public BigDecimal calculateInterest(String accountId, BigDecimal principal, int months) {
BigDecimal rate = new BigDecimal("0.065");
return principal.multiply(rate)
.multiply(BigDecimal.valueOf(months))
.divide(BigDecimal.valueOf(12), 2, RoundingMode.HALF_UP);
}
}
Caller -> CGLIB Proxy -> @Before advice -> Real Method -> @After advice -> Return to caller
The caller never touches the real InterestService object.
The proxy intercepts every call and runs the aspect logic transparently.
What are the five advice types?
Each advice type runs at a different point relative to the target method.
| Advice | Runs When | Receives | Use Case |
|---|---|---|---|
| @Before | Before the method | JoinPoint | Security check, logging entry |
| @After | After (success or exception) | JoinPoint | Cleanup, like finally |
| @AfterReturning | After successful return | JoinPoint + return value | Audit the result |
| @AfterThrowing | After exception | JoinPoint + exception | Error logging, alerting |
| @Around | Wraps the method completely | ProceedingJoinPoint | Timing, caching, retry, circuit breaker |
package com.bank.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@Aspect
@Component
public class TransferAuditAspect {
private static final Logger log = LoggerFactory.getLogger(TransferAuditAspect.class);
@Before("execution(* com.bank.service.TransferService.transfer(..))")
public void beforeTransfer(JoinPoint jp) {
log.info("Starting transfer: {}", java.util.Arrays.toString(jp.getArgs()));
}
@AfterReturning(pointcut = "execution(* com.bank.service.TransferService.transfer(..))",
returning = "result")
public void afterTransfer(JoinPoint jp, Object result) {
log.info("Transfer completed successfully");
}
@AfterThrowing(pointcut = "execution(* com.bank.service.TransferService.transfer(..))",
throwing = "ex")
public void onTransferError(JoinPoint jp, Exception ex) {
log.error("Transfer failed: {}", ex.getMessage());
}
}
How do you create a custom annotation with @Around?
@Around is the most powerful advice type because it completely surrounds the method. It decides what happens before, decides whether to call the real method at all, can modify the arguments, can modify the return value, and runs code after. For timing, you must use @Around because you need to record a start time, let the method run, and record an end time.
package com.bank.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
@Around advice.package com.bank.aspect;
import com.bank.annotation.LogExecutionTime;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TimingAspect {
private static final Logger log = LoggerFactory.getLogger(TimingAspect.class);
@Around("@annotation(com.bank.annotation.LogExecutionTime)")
public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed(); // call the real method
return result;
} finally {
long duration = System.currentTimeMillis() - start;
log.info("{}.{} took {} ms",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
duration);
}
}
}
package com.bank.service;
import com.bank.annotation.LogExecutionTime;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class LoanService {
@LogExecutionTime
public BigDecimal calculateEMI(BigDecimal principal, BigDecimal rate, int months) {
// Pure business logic, no timing boilerplate
BigDecimal monthlyRate = rate.divide(BigDecimal.valueOf(12), 10,
java.math.RoundingMode.HALF_UP);
BigDecimal power = BigDecimal.ONE.add(monthlyRate).pow(months);
return principal.multiply(monthlyRate).multiply(power)
.divide(power.subtract(BigDecimal.ONE), 2, java.math.RoundingMode.HALF_UP);
}
}
INFO LoanService.calculateEMI took 3 ms
What is the self-invocation pitfall?
The single most important consequence of proxy-based AOP is self-invocation. If a method inside your class calls another method of the same class directly using this, that call does not go through the proxy, so the aspect does not run. The proxy only intercepts calls that come in from outside the object.
package com.bank.service;
import com.bank.annotation.LogExecutionTime;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class AccountService {
// WRONG: self-invocation bypasses the proxy
public void processMonthEnd(String accountId) {
// This call goes through "this", NOT through the proxy
// @LogExecutionTime will NOT work
this.calculateInterest(accountId);
}
@LogExecutionTime
public BigDecimal calculateInterest(String accountId) {
// This method's timing will NOT be logged when called from processMonthEnd
return new BigDecimal("1250.00");
}
}
// CORRECT: extract to a separate bean so the call goes through the proxy
package com.bank.service;
import com.bank.annotation.LogExecutionTime;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class InterestCalculationService {
@LogExecutionTime
public BigDecimal calculateInterest(String accountId) {
return new BigDecimal("1250.00");
// Now the proxy intercepts this call because it comes from outside
}
}
@Service
public class AccountService {
private final InterestCalculationService interestService;
public AccountService(InterestCalculationService interestService) {
this.interestService = interestService;
}
public void processMonthEnd(String accountId) {
// This call goes through the CGLIB proxy of InterestCalculationService
// @LogExecutionTime WORKS correctly
interestService.calculateInterest(accountId);
}
}
Self-invocation (WRONG):
AccountService.processMonthEnd() --this.calculateInterest()--> real method
Proxy is bypassed, @LogExecutionTime has no effect
Separate bean (CORRECT):
AccountService --proxy--> InterestCalculationService.calculateInterest()
Proxy intercepts, @LogExecutionTime works
What is the difference between Filter, HandlerInterceptor, and OncePerRequestFilter?
These three are often confused but they operate at different levels in the request lifecycle.
| Aspect | Servlet Filter | HandlerInterceptor | OncePerRequestFilter |
|---|---|---|---|
| Level | Servlet container | Spring MVC | Servlet container |
| Runs | Before DispatcherServlet | After handler mapping | Before DispatcherServlet |
| Knows handler | No | Yes | No |
| Use case | Auth, CORS, logging | Timing, audit context | Same as Filter, guaranteed once |
| Forward behavior | May run twice on forward | Runs once per dispatch | Guaranteed once |
// Servlet Filter: runs at the container level, before DispatcherServlet
package com.bank.filter;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import java.io.IOException;
public class RequestLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, jakarta.servlet.ServletException {
// Runs BEFORE DispatcherServlet -- no idea which controller will handle this
chain.doFilter(request, response);
}
}
// HandlerInterceptor: runs inside Spring MVC, after handler mapping
package com.bank.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
public class AuditInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
// Runs AFTER handler mapping -- knows which controller method will be called
return true; // return false to block the request
}
}
What are common mistakes with AOP?
Besides self-invocation, the most common AOP mistakes involve pointcut expressions and exception handling.
// WRONG: @Around advice that swallows exceptions
@Around("@annotation(com.bank.annotation.LogExecutionTime)")
public Object badAdvice(ProceedingJoinPoint jp) {
try {
return jp.proceed();
} catch (Throwable t) {
log.error("Error", t);
return null; // WRONG: caller receives null instead of exception
}
}
// CORRECT: re-throw the exception after logging
@Around("@annotation(com.bank.annotation.LogExecutionTime)")
public Object goodAdvice(ProceedingJoinPoint jp) throws Throwable {
try {
return jp.proceed();
} catch (Throwable t) {
log.error("Error in {}", jp.getSignature().getName(), t);
throw t; // CORRECT: caller sees the real exception
}
}
What should a senior engineer know?
@Transactionalis implemented as AOP advice. The self-invocation pitfall applies: calling a@Transactionalmethod from within the same class bypasses the transaction proxy, so no transaction is started.- Multiple aspects on the same method execute in order controlled by
@Order. Lower values run first (outer layer of the onion). For National Bank of India, Pankaj orders: security check (@Order(1)) runs first, then transaction (@Order(2)), then timing (@Order(3)). - CGLIB proxies cannot intercept
finalmethods because CGLIB works by subclassing, andfinalmethods cannot be overridden. - AspectJ load-time weaving (LTW) is an alternative to Spring proxies. It modifies bytecode at class-load time, which means self-invocation works and
finalmethods can be intercepted. It is more complex to set up.
package com.bank.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(1) // runs first (outermost)
public class SecurityAspect { /* ... */ }
@Aspect
@Component
@Order(2) // runs second
public class TransactionAspect { /* ... */ }
@Aspect
@Component
@Order(3) // runs last (innermost)
public class TimingAspect { /* ... */ }
Execution order for a method with all three aspects:
SecurityAspect.before()
TransactionAspect.before()
TimingAspect.before()
Real Method executes
TimingAspect.after()
TransactionAspect.after()
SecurityAspect.after()
What are common interview follow-up questions?
- What is the difference between
executionand@annotationpointcuts?executionmatches method signatures (package, class, method name, parameters).@annotationmatches any method annotated with a specific annotation. - Can AOP intercept private methods? No. Spring proxies work at the object boundary, and private methods are not accessible through proxies.
- What happens if
proceed()is not called in@Aroundadvice? The real method never executes. This can be used intentionally to short-circuit a call (for caching or authorization). - How does
@Transactionalrelate to AOP?@Transactionalis implemented as an@Aroundadvice that starts a transaction before the method and commits or rolls back after.
Visual Overview
AOP Proxy Mechanism:
Caller
│
│ method call
▼
┌─────────────────────────┐
│ CGLIB Proxy │
│ │
│ ┌────────────────────┐ │
│ │ @Before advice │ │
│ └────────┬───────────┘ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ Real Method │ │
│ │ (business logic) │ │
│ └────────┬───────────┘ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ @After advice │ │
│ └────────────────────┘ │
└─────────────────────────┘
│
▼
Return to Caller
Self-Invocation Problem:
External Call (works):
Caller ──→ Proxy ──→ @Before ──→ Real Method ──→ @After
Self-Invocation (broken):
Real Method ──this.otherMethod()──→ Real Method directly
Proxy is BYPASSED
Filter vs Interceptor:
HTTP Request
│
┌────┴────────────────┐
│ Servlet Filter │ Raw request, no handler info
│ (OncePerRequestFilter)│
└────┬────────────────┘
▼
┌─────────────────────┐
│ DispatcherServlet │
└────┬────────────────┘
▼
┌─────────────────────┐
│ HandlerMapping │ Finds the controller method
└────┬────────────────┘
▼
┌─────────────────────┐
│ HandlerInterceptor │ Has handler context
│ preHandle() │
└────┬────────────────┘
▼
┌─────────────────────┐
│ Controller Method │
└────┬────────────────┘
▼
┌─────────────────────┐
│ HandlerInterceptor │
│ postHandle() │
└─────────────────────┘
Key Takeaway
AOP separates cross-cutting concerns from business logic by using CGLIB proxies that intercept method calls. In the National Bank of India compliance service, Rajesh's performCheck method has three annotations: @LogExecutionTime for timing, @Auditable for audit logging, and @Transactional for database transaction management. The method body contains only pure business logic. Adding audit logging to a new method requires only one annotation and zero boilerplate.
The critical pitfall is self-invocation. Calling a @Transactional or @LogExecutionTime method from within the same class using this bypasses the proxy, so the aspect does not run. The fix is extracting the method to a separate Spring bean so the call travels through the proxy. Filters run at the Servlet container level before DispatcherServlet for authentication, CORS, and correlation IDs. HandlerInterceptors run inside Spring MVC after handler mapping for timing and audit context. OncePerRequestFilter guarantees single execution even on internal forwards.
Pankaj orders multiple aspects using @Order: security runs first (outermost), then transaction management, then timing (innermost). This layered approach ensures security is checked before a transaction is started, and timing measures the complete duration including transaction overhead.
Remember one sentence: AOP uses proxies to run cross-cutting logic before, after, or around your method, but self-invocation via this bypasses the proxy entirely.
---