skip to content

How would you reason about ordering a custom auditing or security aspect relative to Spring's own transaction management on the same service method?

level: principalimportance: nice to knowfreq 20%

answer

  1. outside vs inside the transaction
  2. tx advice default = LOWEST_PRECEDENCE / MAX_VALUE
  3. @EnableTransactionManagement(order=...)
  4. REQUIRES_NEW to survive rollback
  5. self-invocation defeats proxies

basics

~20 s

Decide which concern must bracket the others and give it the lowest order value so it's outermost. Spring's transaction advice has its own configurable order (default lowest precedence / Integer.MAX_VALUE), so an aspect with a lower value runs outside the transaction; a higher value runs inside it.

solid answer

~50 s

Ordering is a design decision about bracketing. Ask: does my aspect need to run outside or inside the transaction? Spring's transaction interceptor is itself an ordered aspect; by default @EnableTransactionManagement / @Transactional infrastructure sits at Ordered.LOWEST_PRECEDENCE (Integer.MAX_VALUE), and you can change it via the 'order' attribute of @EnableTransactionManagement or on the advisor. To run security/auth checks before a transaction opens, give the security aspect a lower value than the transaction's. To have an audit record participate in the same transaction (rolled back on failure), place it inside — a higher value than the transaction advice, so it runs after begin and before commit. I make these values explicit and centralised, document the intended nesting, and avoid Integer boundaries that collide with framework defaults. I also prefer distinct, spaced values (10, 20, 30) to leave room.

code

java · 15 lines
java
@Configuration
@EnableTransactionManagement(order = 100) // tx advice sits at order 100
class TxConfig {}

// Security OUTSIDE the transaction (rejects before begin):
@Aspect @Component @Order(10)
class SecurityAspect {
    @Before("execution(* svc.*(..))") void authorize() { /* throws before tx opens */ }
}

// Audit INSIDE the transaction (rolled back with the business change):
@Aspect @Component @Order(200) // higher than 100 -> inner -> within tx
class AuditAspect {
    @AfterReturning("execution(* svc.*(..))") void record() { /* same tx */ }
}

go deeper

for a junior

Out of depth; can state lower value = outermost.

for a middle

Can place security outside vs audit inside conceptually.

for a senior

Knows the transaction advisor is ordered and configurable, and inside/outside implications for rollback.

for a principal

Designs the whole onion as a documented contract, accounts for REQUIRES_NEW, self-invocation, and framework defaults.

## Ordering as a bracketing contract For cross-cutting concerns, the real question is **nesting**: which concern must fully enclose which? Because lowest `@Order` value = outermost, you choose values to express the containment you need. ## Spring's transaction advice is itself ordered Spring's declarative transactions (`@Transactional` via `@EnableTransactionManagement` or `<tx:annotation-driven>`) are implemented as an ordered AOP advisor. **By default it runs at `Ordered.LOWEST_PRECEDENCE` (`Integer.MAX_VALUE`)** — i.e. innermost, closest to the target — so most custom aspects naturally sit *outside* the transaction unless you say otherwise. You can move it with the **`order` attribute**: `@EnableTransactionManagement(order = ...)` (and the equivalent on `<tx:annotation-driven order=...>`). The same applies to caching (`@EnableCaching(order=...)`) and async infrastructure. ## Deciding inside vs outside the transaction - **Security / authentication / authorization**: usually **outside** — you want to reject unauthorized calls before opening a transaction. Give it a **lower** value than the transaction advice. - **Audit that must be atomic with the business change**: **inside** the transaction so a rollback also discards the audit row. Give it a **higher** value than the transaction advice (but still runs after begin, before commit). - **Audit that must persist even on failure** (e.g. security incident log): **outside**, and typically in its own transaction (`REQUIRES_NEW`) so the outer rollback doesn't erase it. - **Metrics / timing / logging**: often outermost so they capture the full wall-clock including transaction commit time. ## Concrete nesting example Security `@Order(10)` → Metrics `@Order(20)` → Transaction (default `MAX_VALUE`) → target. On the way out, transaction commits first, then metrics record duration (including commit), then security audits the outcome. ## Practical principles I apply 1. **Make values explicit and centralised** (constants, not magic numbers scattered around). 2. **Space them** (10/20/30) so future concerns slot in without renumbering. 3. **Avoid `Integer.MIN/MAX_VALUE`** unless you deliberately want the extreme, since framework defaults live there. 4. **Document the intended onion** so the interaction is a stated contract, not reverse-engineered. 5. **Prefer `REQUIRES_NEW`** over ordering tricks when a side effect must survive rollback — ordering alone doesn't give you a separate transaction. 6. **Beware self-invocation**: proxy-based AOP (including transactions) doesn't intercept internal `this.method()` calls, which can silently defeat any ordering you designed. ## Interview signal A principal answer connects `@Order` values to transactional semantics (commit/rollback boundaries), knows the transaction advisor's default and its configurable `order`, and treats the nesting as an explicit, documented architectural contract rather than trial-and-error.

  • What is the default order of Spring's declarative transaction advice, and how do you change it?
    By default it is Ordered.LOWEST_PRECEDENCE (Integer.MAX_VALUE), i.e. innermost. You change it via the order attribute of @EnableTransactionManagement (or <tx:annotation-driven order=...>).
  • You need an audit entry to survive even when the business transaction rolls back. Is ordering enough?
    No. Ordering only controls nesting within the same transaction. To persist independently you need a separate transaction, e.g. @Transactional(propagation = REQUIRES_NEW) on the audit path, so the outer rollback doesn't remove it.
  • How can proxy-based AOP silently break your carefully ordered aspects?
    Self-invocation: an internal this.method() call bypasses the proxy, so neither transaction nor custom advice fires, regardless of @Order. Route through the proxy or restructure the call to fix it.

saying these in an interview costs you the question

  • Assuming the transaction always runs outermost by default (it defaults to innermost / LOWEST_PRECEDENCE)
  • Thinking @Order alone can make an audit survive rollback
  • Ignoring self-invocation, which defeats any ordering
  • Using Integer.MIN/MAX_VALUE casually and colliding with framework defaults

context