IntelliJ IDEA Java

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:

  1. Set breakpoints in your createBookmark() and createCategory() methods.
  2. 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 the createCategory() method, the debugger still indicates that the code is running in the same transaction that started in createBookmark().
  • The Revelation: This immediately highlights the problem! Even with @Transactional(propagation = REQUIRES_NEW) on createCategory(), 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 when createBookmark() 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 in CategoryService.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!

image description