[← NESTED and JPA](04-nested-and-jpa.md) · [Index](../README.md) · [Isolation →](06-isolation-and-readonly.md) # 5. Six ways `@Transactional` silently does nothing Sources: [`SilentlyNonTransactional`](../src/main/java/com/ankurm/tx/service/SilentlyNonTransactional.java), [`NotABean`](../src/main/java/com/ankurm/tx/service/NotABean.java). Transcript: [`04-silent-failures.txt`](output/04-silent-failures.txt). Row 0 is the control: the same annotated method reached through the proxy reports `actualTransactionActive=true`. The mechanism works. These six do not use it. ## 1. Self-invocation ```java public String entryPoint() { return annotatedButCalledInternally(); // this. -> no proxy -> no transaction } @Transactional public String annotatedButCalledInternally() { ... } ``` `actualTransactionActive=false`. The proxy wraps the *object*, not its methods; a call the object makes to itself never leaves it. **Fix:** move the method to another bean. Self-injection and `AopContext.currentProxy()` both work and are both worse. ## 2. A private method `@Transactional` on a private method is legal Java and inert: a CGLIB proxy advises by overriding, and private methods cannot be overridden. IntelliJ warns; the compiler does not. ## 3. A checked exception commits ```java @Transactional public void checkedExceptionCommits(String id) throws Exception { accounts.save(new Account(id, 999)); throw new Exception("checked -- this does NOT trigger rollback"); } ``` ``` "3-checked-exception": { "threw": "Exception", "rowSurvived": true, "verdict": "COMMITTED despite the exception" } ``` The default rollback rule is `RuntimeException` or `Error`. A checked exception propagates to the caller **and the transaction commits on the way out**. **Fix:** `@Transactional(rollbackFor = Exception.class)`. ## 4. Swallowing the exception ```java @Transactional public void swallowsException(String id) { accounts.save(new Account(id, 555)); try { throw new IllegalStateException("something went wrong"); } catch (RuntimeException ex) { /* handled */ } } ``` Nothing propagates, so the interceptor sees a normal return and commits. The write survives the failure the code appeared to handle. Failures 3 and 4 are the dangerous pair: they do not merely fail to start a transaction, they **commit work the code was trying to abandon**. ## 5. Called from `@PostConstruct` ``` "5-called-from-post-construct": { "transactionActiveDuringPostConstruct": false } ``` The proxy does not exist while the bean is still initialising, so there is nothing to intercept the call. The reference documentation says not to rely on it; this measures what actually happens. **Fix:** `ApplicationReadyEvent` or `InitializingBean` on a *different* bean. ## 6. An object created with `new` No container, no proxy, no transaction. Survives code review easily because the annotation is right there on the method. ## The one-line check ```java TransactionSynchronizationManager.isActualTransactionActive() ``` Drop it into the method you believe is transactional. If it prints `false`, you have one of these six and no amount of reasoning about propagation will help.