{"id":590817,"date":"2025-08-13T11:08:24","date_gmt":"2025-08-13T10:08:24","guid":{"rendered":"https:\/\/blog.jetbrains.com\/?post_type=idea&#038;p=590817"},"modified":"2025-08-13T11:08:29","modified_gmt":"2025-08-13T10:08:29","slug":"debugging-transactions-let-spring-debugger-do-the-heavy-lifting","status":"publish","type":"idea","link":"https:\/\/blog.jetbrains.com\/zh-hans\/idea\/2025\/08\/debugging-transactions-let-spring-debugger-do-the-heavy-lifting","title":{"rendered":"Debugging Transactions? Let Spring Debugger Do the Heavy Lifting"},"content":{"rendered":"\n<p>Spring Framework&#8217;s <strong><a href=\"https:\/\/docs.spring.io\/spring-framework\/reference\/data-access\/transaction\/declarative.html\" data-type=\"link\" data-id=\"https:\/\/docs.spring.io\/spring-framework\/reference\/data-access\/transaction\/declarative.html\" target=\"_blank\" rel=\"noopener\">declarative transaction management<\/a><\/strong> is incredibly powerful, allowing developers to simply add the <code>@Transactional<\/code> annotation to a method and let Spring handle the transaction details. However, misconfigurations or misunderstandings of how transactions work can lead to frustrating database transaction handling issues. Debugging these problems often means sifting through a lot of AOP proxy code, which is &#8220;not really a pleasant experience&#8221;.<\/p>\n\n\n\n<p>This is where the <a href=\"https:\/\/plugins.jetbrains.com\/plugin\/25302-spring-debugger\" target=\"_blank\" rel=\"noopener\"><strong>Spring Debugger plugin<\/strong><\/a> comes to the rescue! It&#8217;s designed specifically to help you identify and fix these complex transaction management and JPA-related issues, making your debugging much smoother.<\/p>\n\n\n\n<p><strong>You can also watch the demonstration of Spring Debugger plugin\u2019s database transaction debugging feature in this video.<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"><div class=\"wp-block-embed__wrapper\">\n<iframe loading=\"lazy\" title=\"Debugging Transactions? Let Spring Debugger Do the Heavy Lifting\" width=\"500\" height=\"281\" src=\"https:\/\/www.youtube.com\/embed\/smUSH6-Qvn0?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe>\n<\/div><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Unmasking Transactional Mysteries with Spring Debugger<\/h2>\n\n\n\n<p>Let\u2019s say we are building a <a href=\"https:\/\/github.com\/sivaprasadreddy\/spring-debugger-demo\" data-type=\"link\" data-id=\"https:\/\/github.com\/sivaprasadreddy\/spring-debugger-demo\" target=\"_blank\" rel=\"noopener\">bookmarks management application<\/a> and consider a use case creating a new bookmark, which might also involve creating a new bookmark category if it doesn&#8217;t already exist. The goal is that if category creation succeeds but bookmark saving fails, <strong>only the bookmark changes should roll back, while the category creation should persist<\/strong>.<\/p>\n\n\n\n<p>A developer might implement this feature as follows:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@Service\npublic class BookmarkService {\n   \/\/....\n   @Transactional\n   public Bookmark createBookmark(CreateBookmarkCmd cmd) {\n       var bookmark = new Bookmark(cmd.title(), cmd.url());\n       if(cmd.categoryName() != null) {\n           Category category = categoryService.findByName(cmd.categoryName()).orElse(null);\n           if (category == null) {\n               category = this.createCategory(new Category(cmd.categoryName()));\n           }\n           bookmark.setCategory(category);\n       }\n       bookmarkRepository.save(bookmark);\n       log.info(\"Created bookmark with id: {}\", bookmark.getId());\n       return bookmark;\n   }\n\n   @Transactional(propagation = Propagation.REQUIRES_NEW)\n   public Category createCategory(Category category) {\n       category.setId(null);\n       return categoryRepository.save(category);\n   }\n}<\/pre>\n\n\n\n<p>The current implementation assumes that the <code>createBookmark()<\/code> method annotated with <code>@Transactional<\/code> will start a transaction. Inside, if a category needs to be created, call <code>createCategory()<\/code> method which is annotated with <code>@Transactional(propagation = REQUIRES_NEW)<\/code>, hoping it will start a new, independent transaction. The expectation is that if <code>createBookmark()<\/code> fails (e.g., due to a <code>NULL<\/code> URL), the category should still be saved.<\/p>\n\n\n\n<p>Let\u2019s validate this logic through the following test.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"java\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import com.sivalabs.bookmarks.TestcontainersConfig;\nimport org.assertj.core.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.context.annotation.Import;\nimport org.springframework.transaction.TransactionSystemException;\n\nimport java.util.Optional;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.assertj.core.api.Assertions.assertThatThrownBy;\n\n@SpringBootTest\n@Import(TestcontainersConfig.class)\nclass BookmarkServiceTest {\n   @Autowired\n   BookmarkService bookmarkService;\n   @Autowired\n   private CategoryRepository categoryRepository;\n\n   @Test\n   void shouldCreateCategoryEvenWhenBookmarkCreationFails() {\n       String categoryName = \"java25\";\n       Optional&lt;Category> categoryOptional =\n               categoryRepository.findByNameEqualsIgnoreCase(categoryName);\n       assertThat(categoryOptional).isEmpty();\n\n       var cmd = new CreateBookmarkCmd(\n               \"My New Article\",\n               null,\n               categoryName);\n       assertThatThrownBy(()-> bookmarkService.createBookmark(cmd))\n               .isInstanceOf(TransactionSystemException.class);\n\n       categoryOptional = categoryRepository.findByNameEqualsIgnoreCase(categoryName);\n       assertThat(categoryOptional).isNotEmpty();\n   }\n}<\/pre>\n\n\n\n<p>The test will fail and the category was <em>not<\/em> saved, indicating that the intended separate transaction didn&#8217;t actually occur. Why?<\/p>\n\n\n\n<p>Without Spring Debugger, figuring this out would involve painfully sifting through AOP proxy code. But with the plugin, it&#8217;s remarkably simple:<\/p>\n\n\n\n<ol>\n<li><strong>Set breakpoints<\/strong> in your <code>createBookmark()<\/code> and <code>createCategory()<\/code> methods.<\/li>\n\n\n\n<li><strong>Run your test in debug mode<\/strong>.<\/li>\n<\/ol>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1159\" height=\"574\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/08\/spring-debug-txn-1.png\" alt=\"\" class=\"wp-image-590818\"\/><\/figure>\n\n\n\n<p>As you step through the code, the <strong>&#8220;Transaction&#8221; node in the debug tool window<\/strong> provides more insights.<\/p>\n\n\n\n<ul>\n<li><strong>Initial Observation<\/strong>: When in the <code>createBookmark()<\/code> method, the debugger shows that the database transaction started there. Crucially, even after stepping into the <code>createCategory()<\/code> method, the debugger <strong>still indicates that the code is running in the <em>same transaction<\/em> that started in <\/strong><code>createBookmark()<\/code>.<\/li>\n\n\n\n<li><strong>The Revelation<\/strong>: This immediately highlights the problem! Even with <code>@Transactional(propagation = REQUIRES_NEW)<\/code> on <code>createCategory()<\/code>, it&#8217;s not starting a new transaction. The debugger helps us understand <em>why<\/em>: <strong>calling a method within the same class (a &#8220;local method call&#8221;) doesn\u2019t go through the AOP proxy<\/strong>, meaning the <code>@Transactional<\/code> annotation on that internal method is <strong>not taken into consideration<\/strong>. This is why both bookmark and category creation were rolling back together when <code>createBookmark()<\/code> failed.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">The Fix and Visual Validation<\/h2>\n\n\n\n<p>The solution is to <strong>move the <code>createCategory()<\/code> method into a separate service<\/strong> (e.g., <code>CategoryService<\/code>). When the <code>createBookmark()<\/code> method then calls <code>categoryService.createCategory()<\/code>, Spring&#8217;s AOP proxy can intercept this call.<\/p>\n\n\n\n<p>Running the test in debug mode again with the Spring Debugger, you&#8217;ll see a profound difference.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1090\" height=\"719\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/08\/spring-debug-txn-2.png\" alt=\"\" class=\"wp-image-590829\"\/><\/figure>\n\n\n\n<ul>\n<li>When in <code>createBookmark()<\/code>, the debugger shows the parent transaction starting there.<\/li>\n\n\n\n<li>When you step into <code>CategoryService.createCategory()<\/code>, the debugger now clearly indicates that <strong>the <em>current method<\/em> is running in a <em>new transaction<\/em> that started in <\/strong><code><strong>CategoryService.createCategory<\/strong>()<\/code>.<\/li>\n\n\n\n<li>Even better, the debug window shows the <strong>hierarchy of transactions<\/strong>, detailing the parent transaction (<code>BookmarkService.createBookmark()<\/code>) and the newly started child transaction (<code>CategoryService.createCategory()<\/code>).<\/li>\n\n\n\n<li>You also get comprehensive information about each transaction, including its <strong>isolation level, propagation behavior, and read-only status<\/strong>.<\/li>\n\n\n\n<li>You can also see the entities in the Hibernate\u2019s <strong>L1 cache<\/strong>.<\/li>\n<\/ul>\n\n\n\n<p><br>With this change, the code behaves as expected: if bookmark creation fails, only those changes are rolled back, and the category is successfully persisted in the database.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Beyond Transactions: Visualizing JPA Entity States<\/h2>\n\n\n\n<p>Spring Debugger isn&#8217;t just for transactions. It also provides crucial visibility into <strong>JPA entity states<\/strong>, which can be incredibly helpful for debugging persistence-related issues.<\/p>\n\n\n\n<p>As you step through your code, the debugger can show you an entity&#8217;s current state:<\/p>\n\n\n\n<ul>\n<li><strong>TRANSIENT<\/strong>: A newly created object that has not yet been persisted to the database.<\/li>\n\n\n\n<li><strong>DETACHED<\/strong>: An entity that was once managed by the persistence context but is no longer associated with it (e.g., after returning from a service method).<\/li>\n\n\n\n<li><strong>MANAGED<\/strong>: An entity that is currently being tracked by the persistence context.<\/li>\n<\/ul>\n\n\n\n<p>For example, our <code>bookmark<\/code> entity starts in a <strong>TRANSIENT<\/strong> state. The <code>category<\/code> entity, after being created and then returning from <code>CategoryService<\/code> to <code>BookmarkService<\/code>, is shown as <strong>DETACHED<\/strong>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1064\" height=\"395\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/08\/spring-debug-txn-3.png\" alt=\"\" class=\"wp-image-590840\"\/><\/figure>\n\n\n\n<p>After calling <code>bookmarkRepository.save(bookmark)<\/code>, the debugger shows <code>bookmark<\/code> entity state changing to <strong>MANAGED<\/strong>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1045\" height=\"373\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2025\/08\/spring-debug-txn-4.png\" alt=\"\" class=\"wp-image-590851\"\/><\/figure>\n\n\n\n<p>This <strong>visualization of entity state changes<\/strong> is really helpful for debugging these JPA related issues.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>The Spring Debugger plugin is an indispensable tool for any Spring developer. It takes the guesswork out of debugging complex database transaction management issues by <strong>visualizing transaction flow, parent-child transaction hierarchies, and detailed transaction information<\/strong>. Moreover, its ability to show <strong>JPA entity state changes<\/strong> provides deep insights into how your data is being managed.<\/p>\n\n\n\n<p>If you&#8217;ve ever struggled with Spring transaction or JPA-related bugs, go ahead, <strong>install the <\/strong><a href=\"https:\/\/plugins.jetbrains.com\/plugin\/25302-spring-debugger\" target=\"_blank\" rel=\"noopener\"><strong>Spring Debugger plugin<\/strong><\/a> today! You can find more <a href=\"https:\/\/www.jetbrains.com\/help\/idea\/spring-debugger.html\" target=\"_blank\" rel=\"noopener\">documentation<\/a> on it within IntelliJ IDEA documentation as well. Happy debugging!<\/p>\n","protected":false},"author":1517,"featured_media":591234,"comment_status":"closed","ping_status":"closed","template":"","categories":[4759,5088],"tags":[8889,8891,8890],"cross-post-tag":[],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea\/590817"}],"collection":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea"}],"about":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/types\/idea"}],"author":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/users\/1517"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/comments?post=590817"}],"version-history":[{"count":8,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea\/590817\/revisions"}],"predecessor-version":[{"id":592098,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/idea\/590817\/revisions\/592098"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/media\/591234"}],"wp:attachment":[{"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/media?parent=590817"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/categories?post=590817"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/tags?post=590817"},{"taxonomy":"cross-post-tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/zh-hans\/wp-json\/wp\/v2\/cross-post-tag?post=590817"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}