Spring AOP is a favorite interview topic because it tests whether you understand what happens around a method call, not just inside it.
Question: What is Spring AOP, and why is it used?
Answer: Spring AOP is a way to add behavior around Spring bean methods without mixing that code into the business logic. It is mainly used for cross-cutting concerns such as logging, transactions, security, metrics, and caching. Under the hood, Spring usually does this with a proxy, which is a wrapper object that intercepts method calls.
Interview-Ready Answer: I use Spring AOP when I want to keep business code clean and move repeated concerns like logging, timing, security checks, or transaction handling into one place. In Spring Boot, AOP is usually proxy-based, so Spring wraps the bean and intercepts method calls before they reach the real object. One important detail is that self-invocation inside the same class bypasses the proxy, so the advice will not run unless the call goes through the Spring proxy.
Spring AOP solves the cross-cutting concern problem. A cross-cutting concern is logic that belongs in many places, but does not belong to the business rule itself. Examples are audit logs, retries, performance timing, transactions, and authorization checks.
Three words matter in interviews:
In Spring AOP, a join point is usually a method execution on a Spring bean. Weaving means attaching the advice to the target execution path. Spring does this at runtime with a proxy, which is why it is easy to use in Spring Boot.
@Aspect, @Around, @Before, and friends.@Around, it can run code before the call, decide whether to continue, call proceed(), and then run code after the call.Memory model: think of Spring AOP as a bouncer at the club door. If the customer enters through the front door, the bouncer can check them, stamp them, or stop them. If someone sneaks through a side door inside the club, the bouncer never sees it. That is the self-invocation gotcha.
@Before: run just before the method.@AfterReturning: run only if the method succeeds.@AfterThrowing: run only if the method throws.@After: run after completion, success or failure.@Around: wrap the method and control whether it runs at all.| Type | Used when | Main limit |
|---|---|---|
| JDK proxy | Bean has an interface | Only interface methods |
| CGLIB | No interface, or class proxying is chosen | Final methods/classes cannot be advised |
Spring Boot commonly uses proxy-based AOP out of the box once the AOP starter is present. The important thing to remember is not the exact proxy class, but the boundary: the call must cross the proxy for advice to run.
| Tool | Layer | Best for | Weak spot |
|---|---|---|---|
| Spring AOP | Service bean | Method-level concerns | Self calls bypass it |
| Servlet Filter | HTTP request | Headers, auth, tracing | No service-method context |
| HandlerInterceptor | Spring MVC | Controller requests | Not for all bean methods |
Use Spring AOP when the concern belongs to the method, not the HTTP request. Use a filter when the concern belongs to the incoming request, and an interceptor when you need MVC-level request processing.
new is invisible to the proxy system.| Spring AOP | AspectJ weaving |
|---|---|
| Runtime proxy | Bytecode weaving |
| Method execution only | Broader join points |
| Simple setup | More setup power |
| Best for Boot apps | Best for advanced needs |
So the short mental model is: Spring AOP is easy, practical, and perfect for method-level cross-cutting logic, but it is still a proxy system, not magic everywhere.
Real-World Story: Imagine an ecommerce checkout service. The team adds an aspect to measure payment latency and write audit logs whenever a card is charged. For external calls, everything looks great: dashboards show timing, and logs show success or failure. Then a developer refactors the code so placeOrder() calls charge() inside the same service class.
The team assumes the @Around advice still runs, but the internal call never leaves the object, so the proxy is bypassed. Suddenly the payment metrics drop to near zero, audit logs stop showing the real charge call, and support gets reports of missing trace lines during failures. Users may not see the bug directly, but operators see confusing logs, broken dashboards, and inconsistent incident timelines.
What goes wrong in practice:
The fix is to make the call cross the proxy boundary, for example by moving the advised method to another bean or by calling through the current proxy when that is appropriate. This is why AOP knowledge matters in production: a small misunderstanding can hide critical monitoring and make outages much harder to diagnose.
package com.example.springaop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Bean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class SpringAopDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAopDemoApplication.class, args);
}
// In a real project, add spring-boot-starter-web and spring-boot-starter-aop.
@Bean
public TraceAspect traceAspect() {
return new TraceAspect();
}
@Bean
public PaymentService paymentService() {
return new PaymentService();
}
@Bean
public DemoController demoController(PaymentService paymentService) {
return new DemoController(paymentService);
}
@RestController
@RequestMapping("/demo")
public static class DemoController {
private final PaymentService paymentService;
public DemoController(PaymentService paymentService) {
this.paymentService = paymentService;
}
// This call crosses the proxy, so the aspect runs.
@GetMapping("/external/{id}")
public String external(@PathVariable String id) {
return paymentService.charge(id);
}
// Self-invocation example: placeOrder() calls charge() inside the same bean,
// so the inner charge() call bypasses the proxy and the advice does NOT run.
@GetMapping("/self/{id}")
public String self(@PathVariable String id) {
return paymentService.placeOrder(id);
}
// This one uses the current proxy, so the second call is advised too.
@GetMapping("/proxy/{id}")
public String proxy(@PathVariable String id) {
return paymentService.placeOrderViaProxy(id);
}
// Failure path: the aspect logs the exception and rethrows it.
@GetMapping("/fail/{id}")
public String fail(@PathVariable String id) {
return paymentService.charge(id);
}
}
public static class PaymentService {
@Trace
public String charge(String orderId) {
if ("fail".equalsIgnoreCase(orderId)) {
throw new IllegalArgumentException("Invalid order id");
}
return "charged:" + orderId;
}
@Trace
public String placeOrder(String orderId) {
// Direct self-call stays inside the object, so Spring's proxy never sees it.
return "placeOrder -> " + charge(orderId);
}
@Trace
public String placeOrderViaProxy(String orderId) {
// Ask Spring for the active proxy so the nested call goes through advice.
PaymentService proxy = (PaymentService) AopContext.currentProxy();
return "placeOrderViaProxy -> " + proxy.charge(orderId);
}
}
@Aspect
public static class TraceAspect {
@Around(value = "@annotation(trace)", argNames = "pjp,trace")
public Object trace(ProceedingJoinPoint pjp, Trace trace) throws Throwable {
long start = System.nanoTime();
try {
Object result = pjp.proceed();
long tookMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("[AOP] OK " + pjp.getSignature().toShortString() + " in " + tookMs + " ms");
return result;
} catch (Throwable ex) {
long tookMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("[AOP] FAIL " + pjp.getSignature().toShortString() + " in " + tookMs + " ms: " + ex.getMessage());
throw ex;
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Trace {
}
}Follow-up & Tricky Questions:
@Before and @Around? @Before can only run code before the method, while @Around wraps the method and can choose whether the method runs at all. If you need timing, retries, or short-circuiting, @Around is usually the right fit.@Transactional implemented? It is built on Spring AOP-style proxying. That is why transaction boundaries can also be lost on self-invocation.new instead of Spring, will the aspect run? No. Spring only advises beans it manages, so a manually created object is outside the proxy system.@Around, which can inspect or replace the returned object before sending it back to the caller.Tricky / gotcha questions:
@Trace on a method make every call to that method advised? No. The call must go through the Spring proxy; internal calls from the same class still bypass it.Common Mistakes:
Memory Hook: Spring AOP is a bouncer at the bean’s front door: calls that walk through the proxy get checked, stamped, and wrapped; side-door self-calls slip past unnoticed.
Cheat Sheet:
@Around is the most flexible advice type.Practice Tasks:
@AfterThrowing.charge() method into a separate bean and verify that the self-invocation problem disappears.