Sitemap
Rudra Gupta

Rudra Gupta

System Design Enthusiast

6 Practical Spring Boot AOP Implementation Problems Every Java Developer Should Know

6 min readNov 1, 2026

--

From logging and execution monitoring to access control, retry mechanisms, and aspect ordering.

Introduction

Aspect-Oriented Programming (AOP) is one of the important concepts in the Spring Framework. While most developers understand annotations such as @Before, @After, and @Around, the real challenge is knowing when and how to use them in actual applications.

Imagine developing a payment processing application. You need to:

  • Track how long each service method takes.
  • Log method arguments and return values.
  • Capture exceptions.
  • Restrict certain operations to administrators.
  • Retry failed operations.
  • Control the execution order of multiple aspects.

One approach is to implement these features inside every service method. However, this introduces repetitive code and mixes business logic with cross-cutting concerns.

This is where Spring AOP becomes useful.

In this article, we will implement six practical AOP problems that are relevant to real-world Spring Boot development and SDE-1 interviews.

1. Execution Time Monitoring Using @Around

Problem Statement

Create a custom annotation called @TrackExecutionTime.

Whenever a method marked with this annotation executes, calculate and log its execution time in milliseconds.

Step 1: Create a Custom Annotation

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

Here:

  • @Retention(RetentionPolicy.RUNTIME) makes the annotation available at runtime.
  • @Target(ElementType.METHOD) restricts its usage to methods.

Step 2: Create an Aspect

@Aspect
@Component
public class ExecutionTimeAspect {
    @Around("@annotation(TrackExecutionTime)")
public Object measureExecutionTime(
ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis(); try {
return joinPoint.proceed();
} finally {
long end = System.currentTimeMillis();
System.out.println(
"Method: " + joinPoint.getSignature().getName()
+ " took " + (end - start) + " ms"
);
}
}
}

Step 3: Apply the Annotation

@Service
public class PaymentService {
    @TrackExecutionTime
public void processPayment() throws InterruptedException {
Thread.sleep(1000);
System.out.println("Payment processed");
}
}

How does it work?

@Around gives us control over the method invocation.

The important statement is:

joinPoint.proceed();

It allows the original target method to execute.

The finally block ensures that execution time is logged whether the method completes successfully or throws an exception.

Interview Takeaway

Why use @Around instead of @Before and @After?

Because @Around allows us to execute logic both before and after the target method, inspect its return value, modify arguments, and control whether the target method executes.

2. Logging Method Arguments and Return Values

Problem Statement

Create an aspect that logs:

  1. Arguments passed to service methods.
  2. Return values after successful execution.

Implementation

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
public void logArguments(JoinPoint joinPoint) {
System.out.println(
"Method: " + joinPoint.getSignature().getName()
);
System.out.println(
"Arguments: " +
Arrays.toString(joinPoint.getArgs())
);
}
@AfterReturning(
pointcut = "execution(* com.example.service.*.*(..))",
returning = "result"
)
public void logReturnValue(
JoinPoint joinPoint,
Object result) {
System.out.println(
"Returned: " + result
);
}
}

Understanding JoinPoint

JoinPoint provides information about the method invocation.

Some useful methods include:

MethodPurposegetArgs()Returns method argumentsgetSignature()Returns method signaturegetTarget()Returns the target objectgetThis()Returns the proxy objectgetKind()Returns the join point kind

Important Interview Question

Why does @AfterReturning not execute when a method throws an exception?

Because @AfterReturning runs only when the target method completes successfully.

If the method throws an exception, @AfterThrowing is the appropriate advice.

Production consideration: Never blindly log all method arguments or return values. Sensitive information such as passwords, tokens, and payment details must be excluded or masked.

3. Global Exception Logging Using @AfterThrowing

Problem Statement

Implement an aspect that captures exceptions thrown by service-layer methods.

The aspect should log:

  • Method name.
  • Exception type.
  • Exception message.

It must not swallow the exception.

Implementation

@Aspect
@Component
public class ExceptionLoggingAspect {
    @AfterThrowing(
pointcut = "execution(* com.example.service.*.*(..))",
throwing = "exception"
)
public void logException(
JoinPoint joinPoint,
Exception exception) {
System.out.println(
"Method: " +
joinPoint.getSignature().getName()
);
System.out.println(
"Exception: " +
exception.getClass().getSimpleName()
);
System.out.println(
"Message: " +
exception.getMessage()
);
}
}

Test Service

@Service
public class AccountService {
    public void withdrawMoney() {        throw new IllegalArgumentException(
"Insufficient balance"
);
}
}

What happens internally?

  1. The caller invokes withdrawMoney().
  2. The Spring proxy intercepts the invocation.
  3. The target method executes.
  4. An exception is thrown.
  5. The @AfterThrowing advice executes.
  6. The exception continues propagating to the caller.

Interview Takeaway

Does @AfterThrowing handle the exception?

No. It observes the exception and performs additional logic. It does not automatically recover from the exception or suppress it.

If you want to recover, transform the result, or control exception propagation, consider @Around.

4. Restrict Access Using a Custom @AdminOnly Annotation

Problem Statement

Suppose an application has an operation that only administrators should execute.

Instead of repeating role-checking logic in every service method, create a custom annotation called @AdminOnly.

If the current user is not an administrator, throw an access-denied exception.

Step 1: Create Annotation

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

Step 2: Create an Aspect

For demonstration, assume the current user’s role is available through a security service.

@Aspect
@Component
public class SecurityAspect {
    private final UserContext userContext;    public SecurityAspect(UserContext userContext) {
this.userContext = userContext;
}
@Around("@annotation(AdminOnly)")
public Object checkAdminAccess(
ProceedingJoinPoint joinPoint) throws Throwable {
if (!userContext.hasRole("ADMIN")) {
throw new AccessDeniedException(
"Admin access required"
);
}
return joinPoint.proceed();
}
}

Step 3: Apply Annotation

@Service
public class UserService {
    @AdminOnly
public void deleteUser(Long userId) {
System.out.println("User deleted: " + userId);
}
}

Execution Flow

Administrator:

  • Request reaches the proxy.
  • Aspect checks the role.
  • Role is ADMIN.
  • proceed() executes the original method.

Normal User:

  • Request reaches the proxy.
  • Aspect checks the role.
  • Role is not ADMIN.
  • An exception is thrown.
  • Original method never executes.

Interview Takeaway

Why is @Around suitable here?

Get Rudra Gupta’s stories in your inbox

Join Medium for free to get updates from this writer.

Because we need to decide whether the original method should execute at all.

An important production note: for actual application security, prefer Spring Security’s authorization mechanisms such as @PreAuthorize. AOP can be useful for learning or custom cross-cutting policies, but should not replace established security controls without careful design.

5. Implement Retry Mechanism Using @Around

Problem Statement

Imagine calling an external payment gateway. Sometimes the service fails because of temporary network issues.

Instead of immediately failing, retry the operation up to three times.

Create a custom annotation:

@Retry(maxAttempts = 3)

Step 1: Create Annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Retry {
    int maxAttempts() default 3;
}

Step 2: Implement Aspect

@Aspect
@Component
public class RetryAspect {
    @Around("@annotation(retry)")
public Object retryOperation(
ProceedingJoinPoint joinPoint,
Retry retry) throws Throwable {
int attempts = retry.maxAttempts(); for (int i = 1; i <= attempts; i++) { try {
return joinPoint.proceed();
}
catch (TransientException ex) {
System.out.println(
"Attempt " + i + " failed"
);
if (i == attempts) {
throw ex;
}
}
}
return null;
}
}

Step 3: Apply Annotation

@Service
public class PaymentService {
    @Retry(maxAttempts = 3)
public String processPayment() {
System.out.println("Processing payment"); // Simulate temporary failure
throw new TransientException(
"Payment gateway unavailable"
);
}
}

How does proceed() work here?

This is an important concept.

Every time we call:

joinPoint.proceed();

we invoke the target method through the AOP invocation chain.

In this implementation, every loop iteration attempts the method again.

If an attempt succeeds, the return immediately exits the method and returns the result.

If all attempts fail, the final exception is propagated.

Important Interview Follow-up

What if the first attempt successfully performs a database operation, but the response fails afterward?

Blind retries can cause duplicate operations.

For example, a payment might be processed twice.

This is why real retry systems need:

  • Idempotency keys.
  • Retryable exception classification.
  • Exponential backoff.
  • Maximum retry limits.
  • Proper transaction boundaries.

Also, retries should generally be restricted to transient failures rather than every exception.

6. Control Execution Order Using @Order

Problem Statement

Suppose an application has three aspects:

  1. LoggingAspect
  2. SecurityAspect
  3. TimingAspect

We want to control which aspect executes first.

Spring provides the @Order annotation for this purpose.

Step 1: Create Logging Aspect

@Aspect
@Component
@Order(1)
public class LoggingAspect {
    @Around("execution(* com.example.service.*.*(..))")
public Object log(ProceedingJoinPoint joinPoint)
throws Throwable {
System.out.println("Logging: Before"); Object result = joinPoint.proceed(); System.out.println("Logging: After"); return result;
}
}

Step 2: Create Security Aspect

@Aspect
@Component
@Order(2)
public class SecurityAspect {
    @Around("execution(* com.example.service.*.*(..))")
public Object security(ProceedingJoinPoint joinPoint)
throws Throwable {
System.out.println("Security: Before"); Object result = joinPoint.proceed(); System.out.println("Security: After"); return result;
}
}

Step 3: Create Timing Aspect

@Aspect
@Component
@Order(3)
public class TimingAspect {
    @Around("execution(* com.example.service.*.*(..))")
public Object timing(ProceedingJoinPoint joinPoint)
throws Throwable {
System.out.println("Timing: Before"); Object result = joinPoint.proceed(); System.out.println("Timing: After"); return result;
}
}

Expected Execution Flow

When a service method is invoked:

Logging: Before
Security: Before
Timing: Before
Target Method Executes
Timing: After
Security: After
Logging: After

Why does this happen?

Lower @Order values have higher precedence.

The aspect with the highest precedence enters first and exits last.

Think of it like nested method calls.

This becomes especially important when aspects perform security checks, transactions, logging, and performance monitoring.

Interview Takeaway

What happens if the security aspect throws an exception before calling proceed()?

The target method does not execute, and the inner aspects in the invocation chain are not reached.

Final Interview Revision

ProblemMain ConceptAdviceExecution time monitoringCustom annotation@AroundArgument and return loggingJoinPoint@Before, @AfterReturningException loggingException propagation@AfterThrowingAdmin access controlConditional execution@AroundRetry mechanismRepeated invocation@AroundAspect orderingAspect precedence@Order

Conclusion

Spring AOP is not just about learning annotations. Its real value lies in separating cross-cutting concerns from business logic.

These six implementations demonstrate how AOP can be used for monitoring, logging, exception handling, authorization, retry mechanisms, and controlling execution flow.

For SDE-1 interviews, focus on understanding:

  • How Spring proxies intercept method invocations.
  • The difference between JoinPoint and ProceedingJoinPoint.
  • Why proceed() is important in @Around.
  • How exceptions propagate through advice.
  • Why self-invocation bypasses proxy-based advice.
  • How multiple aspects interact through @Order.

The goal is not just to make the code work, but to understand what happens internally when Spring intercepts a method call.

--

--

Rudra Gupta
Rudra Gupta
0 followers

System Design Enthusiast