skip to content

As a principal engineer, how would you decide between @AfterReturning and @Around for a cross-cutting concern like response auditing or metrics, and what pitfalls would you guard against?

level: principalimportance: nice to knowfreq 25%

answer

  1. Least-power principle: observe → AfterReturning
  2. Transform/short-circuit/retry → Around
  3. Metrics incl. failures ≠ AfterReturning
  4. Self-invocation bypasses advice
  5. Advice exceptions poison the call → keep defensive

basics

~20 s

Prefer the least-powerful advice that expresses intent: @AfterReturning for read-only success reactions (audit, metrics, cache-put). Use @Around only when you must transform the result, time the whole call, short-circuit, or handle exceptions. Guard against proxy self-invocation and non-deterministic aspect ordering.

solid answer

~50 s

My default is the principle of least power: @AfterReturning clearly signals 'observe a successful result, change nothing', which is safer and easier to reason about than @Around, where forgetting proceed() silently breaks the target and you must correctly re-throw Throwable. So for success-only auditing, metrics, cache population, or event publishing, @AfterReturning wins. I switch to @Around when the concern inherently needs the full envelope: end-to-end timing (must span before+after, and often failures — so @Around or @After, not @AfterReturning), result transformation/redaction, retries, caching that both reads and writes, or exception translation. Key pitfalls: (1) proxy self-invocation bypasses all advice; (2) @AfterReturning skips exceptions, so it's wrong for total call counts or latency — use @Around/@After there; (3) undefined ordering between equal-@Order aspects; (4) mutating returned objects as a hidden side effect; (5) heavy work in advice on hot paths. I also keep aspects idempotent and exception-safe so advice failures don't corrupt business flow.

code

java · 26 lines
java
@Aspect
@Component
@Order(20) // explicit ordering vs other aspects
public class OrderMetricsAspect {
    private final MeterRegistry meters;
    OrderMetricsAspect(MeterRegistry meters) { this.meters = meters; }

    // Success-only business signal: least-power choice.
    @AfterReturning(
        pointcut = "execution(* com.app.OrderService.place(..))",
        returning = "order")
    public void onPlaced(Order order) {
        try { // defensive: never let a metric failure break a placed order
            meters.counter("orders.placed").increment();
            meters.summary("orders.lines").record(order.getLineCount());
        } catch (RuntimeException ignore) { /* log + swallow */ }
    }

    // Total latency INCLUDING failures must be @Around, not @AfterReturning.
    @Around("execution(* com.app.OrderService.place(..))")
    public Object time(ProceedingJoinPoint pjp) throws Throwable {
        long t = System.nanoTime();
        try { return pjp.proceed(); }
        finally { meters.timer("orders.place.latency").record(System.nanoTime()-t, NANOSECONDS); }
    }
}

go deeper

for a junior

Aware @AfterReturning is for observing success; @Around for changing behavior.

for a middle

Maps concerns to the right advice and knows the exception-skip limitation.

for a senior

Applies least-power reasoning and lists proxy/ordering pitfalls.

for a principal

Governs aspect design: robustness, ordering determinism, hot-path cost, testability, and correct success/failure semantics.

## Framing: principle of least power Cross-cutting concerns should use the **weakest advice that does the job**. `@AfterReturning` is intentionally limited — it can only observe a successful result — and that limitation is a feature: readers instantly know the target's behavior and return value are untouched. `@Around` is maximally powerful (controls args, result, control flow, exceptions) and correspondingly the easiest to break: forgetting `ProceedingJoinPoint.proceed()` silently skips the target; you must declare `throws Throwable` and re-throw; you must cast/handle the `Object` result. Reserve it for cases that genuinely need that envelope. ## Decision guide by concern - **Success audit / 'operation completed' log / domain success event**: `@AfterReturning` — you want it to fire only on success and you need the return value. - **Success-only metrics (e.g. count successful orders, record returned size)**: `@AfterReturning`. - **Total invocation count / latency including failures**: NOT `@AfterReturning` (it skips exceptions). Use `@Around` (wrap timing + try/finally) or split `@AfterReturning` + `@AfterThrowing`, or `@After` for count-regardless. - **Result transformation / redaction / wrapping**: `@Around` (only it can replace the value). - **Retry / circuit-breaking / short-circuit from cache**: `@Around` (needs to control whether/when `proceed()` runs). - **Guaranteed cleanup**: `@After`. - **Failure-only handling / exception translation logging**: `@AfterThrowing` (logging) or `@Around` (to translate/rethrow). ## Pitfalls to guard against 1. **Proxy self-invocation**: Spring AOP is proxy-based; a bean calling its own advised method via `this` bypasses the proxy and no advice fires. Restructure (separate bean), use `AopContext.currentProxy()`, or full AspectJ weaving if unavoidable. 2. **Exception blindness of @AfterReturning**: using it for latency/throughput metrics undercounts failures and skews percentiles. Choose advice that matches the success/failure semantics you need. 3. **Ordering determinism**: equal `@Order` aspects have undefined relative order; always assign explicit orders when aspects interact (security → tx → cache). Remember lower value = higher precedence = outermost. 4. **Hidden mutation**: mutating the returned object from `@AfterReturning` is visible to callers and creates spooky action-at-a-distance; prefer explicit transformation via `@Around` returning a new value. 5. **Advice robustness**: an exception thrown from `@AfterReturning` propagates to the caller and can turn a successful business call into a failure. Keep advice cheap, defensive (try/catch around non-critical side effects), and idempotent; consider async dispatch for expensive audit/metric sinks. 6. **Hot-path cost**: reflection, serialization, or logging large returned payloads in advice adds latency to every call; sample or summarize. 7. **Testability & observability**: advice is invisible at call sites; document aspects and add tests asserting they fire (and don't on exceptions) so behavior isn't accidentally lost via self-invocation or pointcut drift. ## Summary heuristic Ask: 'Do I need to change the call's inputs, output, or control flow, or handle failures?' If **no** and it's success-only → `@AfterReturning`. If **yes** → `@Around` (or `@AfterThrowing`/`@After` for the failure/cleanup slices).

  • Why is @AfterReturning a poor choice for measuring average method latency?
    It runs only on success and can't capture the pre-call timestamp spanning the whole invocation, and it never fires on exceptions — so it undercounts failures and can't measure their latency. Use @Around (start/stop around proceed with try/finally) instead.
  • An audit aspect uses @AfterReturning but sometimes doesn't record events for internally-triggered operations. Likely cause?
    Proxy self-invocation: when the bean calls its own advised method via this, the call bypasses the proxy and no advice runs. Fix by moving the method to a separate bean, using AopContext.currentProxy(), or AspectJ load-time weaving.
  • What happens if your @AfterReturning advice throws an exception?
    It propagates to the caller, converting a successful business call into a failure (and, if inside a transaction proxy ordered outside it, can affect rollback). Keep advice defensive—catch and log non-critical side-effect failures—so observability code can't break business flow.

saying these in an interview costs you the question

  • Defaulting to @Around for everything 'to be safe'
  • Using @AfterReturning for total/latency metrics that must include failures
  • Ignoring proxy self-invocation as a reason advice silently doesn't fire
  • Letting advice throw and break the underlying business call
  • Not setting explicit @Order when aspects interact

context