IntelliJ IDEA
IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin
Debugging Transactions? Let Spring Debugger Do the Heavy Lifting
Spring Framework’s declarative transaction management is incredibly powerful, allowing developers to simply add the @Transactional
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 “not really a pleasant experience”.
This is where the Spring Debugger plugin comes to the rescue! It’s designed specifically to help you identify and fix these complex transaction management and JPA-related issues, making your debugging much smoother.
You can also watch the demonstration of Spring Debugger plugin’s database transaction debugging feature in this video.
Unmasking Transactional Mysteries with Spring Debugger
Let’s say we are building a bookmarks management application and consider a use case creating a new bookmark, which might also involve creating a new bookmark category if it doesn’t already exist. The goal is that if category creation succeeds but bookmark saving fails, only the bookmark changes should roll back, while the category creation should persist.
A developer might implement this feature as follows:
@Service public class BookmarkService { //.... @Transactional public Bookmark createBookmark(CreateBookmarkCmd cmd) { var bookmark = new Bookmark(cmd.title(), cmd.url()); if(cmd.categoryName() != null) { Category category = categoryService.findByName(cmd.categoryName()).orElse(null); if (category == null) { category = this.createCategory(new Category(cmd.categoryName())); } bookmark.setCategory(category); } bookmarkRepository.save(bookmark); log.info("Created bookmark with id: {}", bookmark.getId()); return bookmark; } @Transactional(propagation = Propagation.REQUIRES_NEW) public Category createCategory(Category category) { category.setId(null); return categoryRepository.save(category); } }
The current implementation assumes that the createBookmark()
method annotated with @Transactional
will start a transaction. Inside, if a category needs to be created, call createCategory()
method which is annotated with @Transactional(propagation = REQUIRES_NEW)
, hoping it will start a new, independent transaction. The expectation is that if createBookmark()
fails (e.g., due to a NULL
URL), the category should still be saved.
Let’s validate this logic through the following test.
import com.sivalabs.bookmarks.TestcontainersConfig; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; import org.springframework.transaction.TransactionSystemException; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @SpringBootTest @Import(TestcontainersConfig.class) class BookmarkServiceTest { @Autowired BookmarkService bookmarkService; @Autowired private CategoryRepository categoryRepository; @Test void shouldCreateCategoryEvenWhenBookmarkCreationFails() { String categoryName = "java25"; Optional<Category> categoryOptional = categoryRepository.findByNameEqualsIgnoreCase(categoryName); assertThat(categoryOptional).isEmpty(); var cmd = new CreateBookmarkCmd( "My New Article", null, categoryName); assertThatThrownBy(()-> bookmarkService.createBookmark(cmd)) .isInstanceOf(TransactionSystemException.class); categoryOptional = categoryRepository.findByNameEqualsIgnoreCase(categoryName); assertThat(categoryOptional).isNotEmpty(); } }
The test will fail and the category was not saved, indicating that the intended separate transaction didn’t actually occur. Why?
Without Spring Debugger, figuring this out would involve painfully sifting through AOP proxy code. But with the plugin, it’s remarkably simple:
- Set breakpoints in your
createBookmark()
andcreateCategory()
methods. - Run your test in debug mode.

As you step through the code, the “Transaction” node in the debug tool window provides more insights.
- Initial Observation: When in the
createBookmark()
method, the debugger shows that the database transaction started there. Crucially, even after stepping into thecreateCategory()
method, the debugger still indicates that the code is running in the same transaction that started increateBookmark()
. - The Revelation: This immediately highlights the problem! Even with
@Transactional(propagation = REQUIRES_NEW)
oncreateCategory()
, it’s not starting a new transaction. The debugger helps us understand why: calling a method within the same class (a “local method call”) doesn’t go through the AOP proxy, meaning the@Transactional
annotation on that internal method is not taken into consideration. This is why both bookmark and category creation were rolling back together whencreateBookmark()
failed.
The Fix and Visual Validation
The solution is to move the createCategory()
method into a separate service (e.g., CategoryService
). When the createBookmark()
method then calls categoryService.createCategory()
, Spring’s AOP proxy can intercept this call.
Running the test in debug mode again with the Spring Debugger, you’ll see a profound difference.

- When in
createBookmark()
, the debugger shows the parent transaction starting there. - When you step into
CategoryService.createCategory()
, the debugger now clearly indicates that the current method is running in a new transaction that started inCategoryService.createCategory()
. - Even better, the debug window shows the hierarchy of transactions, detailing the parent transaction (
BookmarkService.createBookmark()
) and the newly started child transaction (CategoryService.createCategory()
). - You also get comprehensive information about each transaction, including its isolation level, propagation behavior, and read-only status.
- You can also see the entities in the Hibernate’s L1 cache.
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.
Beyond Transactions: Visualizing JPA Entity States
Spring Debugger isn’t just for transactions. It also provides crucial visibility into JPA entity states, which can be incredibly helpful for debugging persistence-related issues.
As you step through your code, the debugger can show you an entity’s current state:
- TRANSIENT: A newly created object that has not yet been persisted to the database.
- DETACHED: 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).
- MANAGED: An entity that is currently being tracked by the persistence context.
For example, our bookmark
entity starts in a TRANSIENT state. The category
entity, after being created and then returning from CategoryService
to BookmarkService
, is shown as DETACHED.

After calling bookmarkRepository.save(bookmark)
, the debugger shows bookmark
entity state changing to MANAGED.

This visualization of entity state changes is really helpful for debugging these JPA related issues.
Conclusion
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 visualizing transaction flow, parent-child transaction hierarchies, and detailed transaction information. Moreover, its ability to show JPA entity state changes provides deep insights into how your data is being managed.
If you’ve ever struggled with Spring transaction or JPA-related bugs, go ahead, install the Spring Debugger plugin today! You can find more documentation on it within IntelliJ IDEA documentation as well. Happy debugging!