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?
answer
- Least-power principle: observe → AfterReturning
- Transform/short-circuit/retry → Around
- Metrics incl. failures ≠ AfterReturning
- Self-invocation bypasses advice
- Advice exceptions poison the call → keep defensive
basics
~20 sPrefer 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 sMy 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@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
Aware @AfterReturning is for observing success; @Around for changing behavior.
Maps concerns to the right advice and knows the exception-skip limitation.
Applies least-power reasoning and lists proxy/ordering pitfalls.
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