skip to content

How does RuleBasedTransactionAttribute decide commit vs rollback when multiple rollbackFor/noRollbackFor rules could match the thrown exception?

level: seniorimportance: should knowfreq 40%

answer

  1. RuleBasedTransactionAttribute holds the rule list
  2. getDepth = hops up hierarchy to a name match
  3. Smallest depth wins, order irrelevant
  4. NoRollbackRuleAttribute winner -> commit
  5. No match -> super = RuntimeException||Error

basics

~20 s

Spring stores the rules in a RuleBasedTransactionAttribute. For a thrown exception it picks the rule whose type is the CLOSEST match in the exception's class hierarchy (smallest depth). If that winning rule is a noRollbackFor rule it commits; if rollbackFor, it rolls back. If no rule matches, it uses the default (rollback on RuntimeException/Error).

solid answer

~40 s

Each rollbackFor/noRollbackFor entry becomes a RollbackRuleAttribute or NoRollbackRuleAttribute, held in a RuleBasedTransactionAttribute. When an exception propagates, rollbackOn walks each rule and computes a 'depth' — how many superclass hops from the thrown exception's class up to the rule's declared type (a match). The rule with the SMALLEST depth (the most specific match) wins, regardless of declaration order. If the winner is a NoRollbackRuleAttribute, Spring commits; if a RollbackRuleAttribute, it rolls back. If no rule matches at all, it delegates to the superclass DefaultTransactionAttribute.rollbackOn, i.e. the default RuntimeException/Error rule. So specificity, not order and not rollbackFor-vs-noRollbackFor priority, resolves conflicts — the exception type closest in the hierarchy governs.

code

java · 15 lines
java
@Transactional(
    rollbackFor = Exception.class,             // broad: depth-far
    noRollbackFor = IllegalArgumentException.class // narrow: depth-near
)
public void process() {
    // throw new NumberFormatException()  -> noRollback wins (depth 1) -> COMMIT
    // throw new IOException()            -> only rollbackFor matches   -> ROLLBACK
    // throw new IllegalStateException()  -> no rule matches -> default -> ROLLBACK
}

// Equivalent programmatic construction:
// RuleBasedTransactionAttribute attr = new RuleBasedTransactionAttribute();
// attr.setRollbackRules(List.of(
//     new RollbackRuleAttribute(Exception.class),
//     new NoRollbackRuleAttribute(IllegalArgumentException.class)));

go deeper

for a junior

Not expected to know the internal precedence engine.

for a middle

Should at least know closest-match-wins conceptually.

for a senior

Can explain getDepth, the winner logic, and the fallthrough to super.rollbackOn.

for a principal

Reasons about deterministic conflict resolution when composing broad+narrow policies and the name-substring matching implications.

## The engine behind the attributes: RuleBasedTransactionAttribute When you write `@Transactional(rollbackFor = X.class, noRollbackFor = Y.class)`, Spring builds a **`RuleBasedTransactionAttribute`** (a subclass of `DefaultTransactionAttribute`) holding a `List<RollbackRuleAttribute>` where each element is either: - a **`RollbackRuleAttribute`** (from `rollbackFor` / `rollbackForClassName`), or - a **`NoRollbackRuleAttribute`** (a subclass of `RollbackRuleAttribute`, from `noRollbackFor` / `noRollbackForClassName`). ### How a decision is made — `rollbackOn(Throwable ex)` Roughly: ```java RollbackRuleAttribute winner = null; int deepest = Integer.MAX_VALUE; for (RollbackRuleAttribute rule : this.rollbackRules) { int depth = rule.getDepth(ex); // -1 = no match if (depth >= 0 && depth < deepest) { deepest = depth; winner = rule; } } if (winner == null) { return super.rollbackOn(ex); // DEFAULT: RuntimeException || Error } return !(winner instanceof NoRollbackRuleAttribute); ``` ### What is 'depth'? `RollbackRuleAttribute.getDepth(Throwable)` counts how many steps up the exception's class hierarchy you must climb before a class's name **contains** the rule's pattern string. - Depth `0` = the thrown class itself matches. - Depth `1` = its immediate superclass matches, etc. - `-1` = never matches. (Even when you pass a `Class`, the rule stores its `getName()` and matches by **name substring**, which is why the String-based gotcha exists.) ### The decisive rule: closest match wins The rule with the **smallest non-negative depth** wins. Declaration order is irrelevant, and there is **no inherent priority** between rollback and no-rollback rules — **specificity in the class hierarchy** decides. #### Worked example ```java @Transactional(rollbackFor = Exception.class, noRollbackFor = IllegalArgumentException.class) ``` Throw `IllegalArgumentException`: - `noRollbackFor` rule (`IllegalArgumentException`): depth **0**. - `rollbackFor` rule (`Exception`): depth 3 (IAE -> RuntimeException -> Exception... actually IllegalArgumentException -> RuntimeException -> Exception = depth 2). - Winner = depth 0 = the **NoRollback** rule -> **commit**. Throw `NumberFormatException` (a subclass of `IllegalArgumentException`): - noRollbackFor(`IllegalArgumentException`): depth 1. - rollbackFor(`Exception`): depth 3. - Winner = noRollback (depth 1) -> **commit**. Throw a plain `IOException`: - noRollbackFor(`IllegalArgumentException`): -1 (no match). - rollbackFor(`Exception`): depth 1. - Winner = rollback -> **roll back**. ### Fallthrough to default If *no* rule matches (`winner == null`), Spring calls `super.rollbackOn`, the plain `RuntimeException || Error` default — so declaring only `rollbackFor = SomeChecked.class` still leaves ordinary `RuntimeException`s rolling back via the default. ### Why this matters - You can safely combine broad and narrow rules (`rollbackFor = Exception.class` + `noRollbackFor = SpecificException.class`) and rely on the specific one winning. - Ambiguity ('which rule wins?') has a deterministic answer: **the closest ancestor**. - The name-substring matching means poorly chosen `*ClassName` patterns can match unexpectedly and skew depth.

  • If rollbackFor and noRollbackFor name the exact same class, what happens?
    Both rules match at depth 0. Spring iterates and keeps the first rule that is strictly shallower than the current best; with equal depth the earlier-added rule is retained. In practice rollbackFor rules are added before noRollbackFor, so the rollback rule tends to win — but relying on this is fragile; don't declare contradictory same-type rules.
  • Does declaration order in the annotation matter?
    Not for different depths — the closest match always wins. Order only becomes a tiebreaker at identical depth, which you should avoid by not declaring conflicting equal-specificity rules.

saying these in an interview costs you the question

  • Claiming noRollbackFor always overrides rollbackFor (or vice versa) regardless of specificity
  • Saying the last-declared rule wins
  • Thinking depth is about call-stack depth rather than class-hierarchy distance
  • Forgetting the fallthrough to the default RuntimeException/Error rule when nothing matches

context