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).
// 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.