RizTech Academy logo
RizTech Academy
Patterns in SpringLesson 2 of 535 min

AOP and cross-cutting concerns, without the magic

Some concerns cut across many methods — logging every service call, timing slow operations, an audit trail, and (though the framework does it for you) transactions. Repeating that code in every method is noise; Aspect-Oriented Programming (AOP) lets you write it once and apply it declaratively wherever it belongs. AOP is also the mechanism behind @Transactional and @Async, so understanding it demystifies those. This lesson is AOP without the magic — what it is, how Spring does it, and when it earns its place.

What a cross-cutting concern is

A cross-cutting concern is behaviour needed in many places but not part of any one method's core logic: logging that a method was called, measuring how long it took, checking a permission, wrapping in a transaction. If you write it inline, it clutters every method and is repeated everywhere; change the logging format and you edit a hundred methods. AOP extracts that behaviour into one place — an aspect — and applies it to the methods you specify, without touching their code. @Transactional is exactly this: the transaction-management concern, applied to any method you annotate, written once by the framework.

The vocabulary, briefly

AOP has a small vocabulary worth knowing:

  • Aspect — the module holding the cross-cutting behaviour (a class annotated @Aspect).
  • Advice — the code to run (@Before, @After, @Around a method).
  • Pointcut — an expression selecting which methods the advice applies to (e.g. "all methods in the service package").
  • Join point — a specific point where advice runs (a method execution).

In practice: you write an aspect with advice and a pointcut, and Spring applies the advice at the matching join points.

An aspect: logging service-method timing

A concrete, useful aspect — log how long every service method takes:

@Aspect
@Component
public class TimingAspect {

    private static final Logger log = LoggerFactory.getLogger(TimingAspect.class);

    @Around("execution(* com.riztech.dakiya..*Service.*(..))")   // pointcut: any *Service method
    public Object time(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return pjp.proceed();                                 // run the actual method
        } finally {
            long ms = System.currentTimeMillis() - start;
            log.info("{} took {} ms", pjp.getSignature().toShortString(), ms);
        }
    }
}

@Around advice wraps the method: pjp.proceed() runs the real method, and the aspect times it and logs — for every method matching the pointcut execution(* ...*Service.*(..)), with no logging code in any service. Change the timing/logging once, in the aspect, and it applies everywhere. That is AOP's value: the concern lives in one place and is applied declaratively by a pointcut.

How Spring AOP works: proxies (and its limits)

Spring AOP works by the proxy mechanism you already met with @Transactional: Spring wraps the target bean in a proxy that runs the advice around the real method. This is the same mechanism, so it has the same consequences — the two you must remember:

  • It only applies to Spring-managed beans, called across bean boundaries. A self-invocation (a method calling another method of the same class) bypasses the proxy, so the advice does not run — the identical trap as @Transactional. This is why those annotations have that limitation: they are AOP.
  • It advises method executions on beans, not arbitrary code, constructors, or fields.

Understanding "it is a proxy" explains both the power (declarative cross-cutting behaviour) and the gotchas (self-invocation, beans only) of @Transactional, @Async, and your own aspects in one idea.

When AOP earns its place — and when not

AOP is powerful and, like all power, easy to overuse. The honest guidance:

  • Good uses: genuinely cross-cutting, technical concerns applied broadly — logging/tracing, performance timing, metrics, auditing, custom security checks, retry. Behaviour that is the same across many methods and not part of their business logic. (And of course the framework's own @Transactional/@Async, which you use rather than write.)
  • Poor uses: business logic. Do not put domain rules in aspects — that scatters the logic invisibly (the reader of a service method cannot see that an aspect is changing its behaviour), which is exactly the "action at a distance" problem the Django signals lesson warned about. Business logic belongs in services, visibly; aspects are for technical cross-cutting concerns.
  • The over-engineering risk: custom aspects for things that are not really cross-cutting, or a maze of pointcuts that make behaviour impossible to trace. Most applications need few or no custom aspects — the framework's built-in AOP (@Transactional) covers the common case. Reach for a custom aspect when a technical concern genuinely repeats across many methods; otherwise do not.

The rule: use AOP for cross-cutting technical concerns that genuinely repeat (logging, metrics, auditing), never for business logic, and sparingly — and understand it mainly because it is the mechanism behind @Transactional and @Async, whose behaviour you now fully understand.

Check your work

Cross-cutting concern. Behaviour needed across many methods but not core to any (logging, timing, audit, transactions); AOP extracts it into one aspect applied declaratively, instead of repeating it.

Vocabulary. Aspect (the module), advice (@Before/@After/@Around — the code), pointcut (which methods), join point (where it runs).

How Spring does it. By proxies — the same mechanism as @Transactional/@Async — so it advises method executions on beans and self-invocation bypasses it (the identical trap). Understanding "it's a proxy" explains those annotations' power and gotchas.

When to use it. Cross-cutting technical concerns that repeat (logging, timing, metrics, audit, retry) — not business logic (that scatters it invisibly, like signals), and sparingly (most apps need few/no custom aspects; the built-in @Transactional covers the common case).

Practice

  1. Write a @Around TimingAspect over *Service methods that logs the duration; confirm it runs for service methods with no logging code in them.
  2. Call an aspected method via self-invocation and confirm the advice does not run (proxy bypassed) — connect this to the @Transactional self-invocation trap.
  3. Narrow the pointcut to a single class or annotation and confirm the advice applies only there.
  4. Try putting a business rule in an aspect, then argue why that "action at a distance" is worse than the rule living visibly in the service.
  5. List three concerns in Dakiya that are genuinely cross-cutting (technical) and three that are business logic; decide which (if any) warrant an aspect.
  6. Explain how AOP being proxy-based accounts for both the usefulness and the self-invocation limitation of @Transactional.

Official documentation

Next: service boundaries and when to add an abstraction.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship