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,@Arounda 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
- Write a
@AroundTimingAspectover*Servicemethods that logs the duration; confirm it runs for service methods with no logging code in them. - Call an aspected method via self-invocation and confirm the advice does not run (proxy bypassed) —
connect this to the
@Transactionalself-invocation trap. - Narrow the pointcut to a single class or annotation and confirm the advice applies only there.
- 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.
- List three concerns in Dakiya that are genuinely cross-cutting (technical) and three that are business logic; decide which (if any) warrant an aspect.
- Explain how AOP being proxy-based accounts for both the usefulness and the self-invocation limitation of
@Transactional.
Official documentation
- Spring — Aspect-Oriented Programming with Spring — Aspects, advice, pointcuts, proxies.
- Spring — @Around advice and pointcut expressions — Writing advice and pointcuts.
- Spring — Proxying mechanisms — Why self-invocation bypasses advice.
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