Interview Prep Zoneby CuriouserLabs

Question 5 of 5 · architect level

Beyond @Transactional: where does AOP earn its keep in a real system, and where does it become a liability?

🎤 Say this first

AOP earns its keep for genuinely cross-cutting, business-logic-free concerns with stable semantics: transactions, caching (@Cacheable), retries/circuit breaking (Resilience4j annotations), method security (@PreAuthorize), idempotency keys, audit trails, metrics/tracing. It becomes a liability when business rules hide in aspects (invisible control flow), when aspect ordering encodes correctness nobody documented, when pointcuts sweep too broadly, and when teams debug 'action at a distance' they can't see in the code. Rule: aspects may observe and manage, they must not decide.

The full picture

  • The annotation-as-contract pattern: @Retry(name="payment"), @RateLimiter, @Idempotent — call sites document behavior; implementations stay swappable; grep-ability preserved.
  • Ordering as architecture: define and document the stack once — e.g., Metrics(outermost) → Security → RateLimit → Retry → CircuitBreaker → Transaction(innermost). Retry outside transaction (fresh TX per attempt) vs inside (retrying a poisoned TX) is a correctness decision, not a style one.
  • Liability patterns: aspects mutating arguments/return values of business calls; pointcuts on naming conventions (*Service.save*) that new code accidentally matches; caching aspects hiding staleness bugs; anything a new hire can't discover by reading the method.
  • Observability of the aspects themselves: they run on every hot path — they need their own budgets (an aspect doing sync logging can dominate p99).
Declared, documented, bounded
// The whole resilience story, visible at the call site:
@Retry(name = "inventory", fallbackMethod = "cachedLevels")
@CircuitBreaker(name = "inventory")
@TimeLimiter(name = "inventory")
public CompletableFuture<Levels> levels(SkuId sku) { … }

// vs the liability version: a pointcut somewhere in aop/ package
// silently retrying every method named get* in com.app.client..*
// — nobody at THIS call site knows retries exist. Grep finds nothing.

🔄 Likely follow-up questions

  • How do you document and enforce aspect ordering across a codebase?
  • Cache invalidation with @CacheEvict — where does declarative caching break down?
  • How do you make aspect behavior visible in traces so on-call can see it?