== six pieces of code carrying @Transactional that are not transactional == Row 0 is the control: the SAME annotated method, reached through the proxy. { "0-control-through-the-proxy": "through the proxy: actualTransactionActive=true", "1-self-invocation": "self-invocation: actualTransactionActive=false", "2-private-method": "private method: actualTransactionActive=false", "3-checked-exception": { "threw": "Exception", "rowSurvived": true, "verdict": "COMMITTED despite the exception" }, "4-swallowed-exception": { "rowSurvived": true, "verdict": "COMMITTED -- the exception never reached the interceptor" }, "5-called-from-post-construct": { "transactionActiveDuringPostConstruct": false }, "6-created-with-new": "created with new: actualTransactionActive=false" } Reading it: 0 control actualTransactionActive=true. The mechanism works. 1 self-invocation entryPoint() is not annotated and calls this.annotated...(), so the proxy is never involved. Same class, same annotation, no transaction. 2 private method a CGLIB proxy advises by overriding, and private methods cannot be overridden. Legal Java, no effect. 3 checked exception the default rollback rule is RuntimeException or Error. A checked exception propagates AND the transaction commits. Fix: @Transactional(rollbackFor = Exception.class). 4 swallowed nothing propagates, so the interceptor sees a normal return and commits. The write survives the failure it 'handled'. 5 @PostConstruct the proxy does not exist yet during initialisation. 6 new no container, no proxy, no transaction. Note what rows 3 and 4 have in common: the row is still there afterwards. These two do not merely fail to start a transaction -- they start one and COMMIT work that the code was trying to abandon.