skip to content

When should you choose @Around over @Before/@After* advice, and what design and correctness risks does @Around introduce?

level: principalimportance: should knowfreq 40%

answer

  1. least-power: narrowest advice wins
  2. @Around = only one that alters return/exception/flow
  3. dropped-return + swallowed-exception risks
  4. proxy: self-invocation, CGLIB no-final
  5. prefer @Transactional/@Cacheable/@Retryable

basics

~20 s

Use @Around only when you must control whether/how the method runs or change its result or exceptions — timing, caching, retry, transactions. For pure side effects (logging, auditing), use the narrower @Before/@After* which can't accidentally drop the return value.

solid answer

~50 s

@Around is the only advice that wraps execution, so it alone can short-circuit, rewrite arguments, transform the return value, and fully control the exception path. That power is also its risk: it must remember to call proceed() and return its result (or callers silently get null), it can swallow exceptions, and it obscures control flow. The principle is least-power: choose the narrowest advice that does the job. Use @Before for pre-checks/setup, @AfterReturning for post-success work, @AfterThrowing for error logging, @After for cleanup — none can corrupt the return value. Reserve @Around for genuine wrapping needs: timing, caching, retry, rate-limiting, argument normalization, exception translation. Also weigh proxy limitations (self-invocation bypass, final classes/methods with JDK vs CGLIB proxies), aspect ordering, performance of reflective proceed(), and testability/observability. Prefer battle-tested abstractions (@Transactional, @Cacheable, @Retryable, Resilience4j) over hand-rolled @Around when they exist.

code

java · 13 lines
java
// Overkill: @Around used for pure logging -> risks dropping the return value
@Around("execution(* com.example..*(..))")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
    Object r = pjp.proceed();
    logger.info("{} returned", pjp.getSignature());
    return r; // easy to forget -> callers get null
}

// Better: narrower advice can't corrupt the return value or hide errors
@AfterReturning(pointcut = "execution(* com.example..*(..))", returning = "r")
public void logSuccess(JoinPoint jp, Object r) {
    logger.info("{} returned {}", jp.getSignature(), r);
}

go deeper

for a junior

Know @Around is more powerful but the other advice types exist for simpler cases.

for a middle

Match each need (log/cleanup/timing/cache) to the appropriate advice type.

for a senior

Articulate the least-power principle and the correctness risks of @Around plus proxy limitations.

for a principal

Drive architectural decisions: prefer standard abstractions, scope pointcuts tightly, manage aspect ordering, and ensure aspect behavior is tested and observable.

## The principle: least power All five advice types run around a method, but they differ in **how much they can do**: | Advice | Runs | Can change return? | Can swallow exception? | Can skip method? | |---|---|---|---|---| | `@Before` | before | no | no | no | | `@AfterReturning` | after success | can read return (bind), not replace | no | no | | `@AfterThrowing` | on throw | no | no | no | | `@After` | finally | no | no | no | | `@Around` | wraps | **yes** | **yes** | **yes** | Choose the **narrowest** advice that accomplishes the task. `@Around` should be a deliberate choice, not the default, precisely because it can do things the others cannot — including things you didn't intend. ## When @Around is the right tool - **Timing / metrics** around the call. - **Caching** — short-circuit on hit. - **Retry / circuit breaker** — repeated or conditional proceed(). - **Rate limiting / feature flags** — reject before proceed(). - **Argument normalization** — proceed(Object[]). - **Return-value transformation** — wrap/redact the result. - **Exception translation** at an architectural boundary. If you don't need any of those — e.g. you just log method entry, audit a successful call, or release a resource — use `@Before`/`@AfterReturning`/`@After`. They are simpler and structurally cannot drop the return value or hide an exception. ## Correctness risks unique to @Around 1. **Dropped return value** — forget to `return pjp.proceed()` and every caller silently gets `null`. The single most common AOP bug. 2. **Swallowed exceptions** — a catch that neither rethrows nor returns a valid value hides failures. 3. **Control-flow opacity** — readers of the target method can't see that it may not run, may run twice, or may return a substitute value. 4. **Broken invariants under retry** — non-idempotent side effects duplicated. 5. **throws Throwable leakage** — advice signature must declare it, which can loosen the error contract if not handled. ## Proxy-level considerations (apply to all advice, sharpest for @Around) - **Self-invocation:** `this.method()` inside the same bean bypasses the proxy, so the aspect never fires. Refactor to call through the injected proxy, or use AspectJ load-time weaving. - **JDK dynamic proxy vs CGLIB:** JDK proxies require an interface; CGLIB subclasses the class and **cannot proxy final classes/methods** or private methods. Spring Boot defaults to CGLIB (`proxyTargetClass=true`). - **Performance:** `proceed()` is a reflective-ish invocation with per-call overhead; fine for I/O-bound service methods, questionable for hot, tiny methods called in tight loops. - **Ordering:** multiple aspects nest by `@Order`/`Ordered`; lowest order = outermost. Getting retry vs transaction vs security nesting wrong causes subtle bugs. ## Prefer proven abstractions Most `@Around` needs already have hardened implementations: - Transactions → `@Transactional`. - Caching → `@Cacheable`/`@CacheEvict`. - Retry → Spring Retry `@Retryable` / Resilience4j. - Rate limit / circuit breaker → Resilience4j. - Metrics → Micrometer `@Timed`. Hand-rolled `@Around` is justified when no standard abstraction fits, and it should then be small, well-tested, and narrowly pointcut-scoped. ## Testability & observability Aspects add indirection that unit tests of the target don't see (tests call the plain object, not the proxy). Cover aspect behavior with **integration tests through the Spring context**. Ensure swallowed/translated exceptions are still logged/metered so failures remain visible. ## Summary `@Around` is a scalpel: unmatched control, but easy to cut yourself. Default to narrower advice; reach for `@Around` only when wrapping is genuinely required; and prefer framework-provided cross-cutting abstractions over bespoke aspects.

  • You need to log every method's return value. Which advice should you use and why?
    @AfterReturning with a returning="result" binding. It gives you read access to the return value without the ability to replace it or swallow exceptions, so it cannot accidentally corrupt behavior — unlike @Around, which would require you to remember to return proceed()'s result.
  • Why might a hand-rolled @Around retry aspect be worse than Spring Retry / Resilience4j?
    The library handles backoff, jitter, max attempts, exception classification, recovery callbacks, metrics, and thread-safety that a naive aspect misses. It's tested and configurable, avoids reinventing edge cases, and integrates ordering/observability. Hand-rolled @Around is justified only when no standard abstraction fits.

saying these in an interview costs you the question

  • Using @Around by default for everything, including pure logging
  • Not knowing @Before/@After* cannot change the return value (that's the point)
  • Ignoring proxy self-invocation and final-class/CGLIB limitations
  • Reinventing transactions/caching/retry with bespoke @Around instead of standard abstractions
  • Overlooking aspect ordering when multiple @Around apply

context