CodeGym /Courses /Module 5. Spring /Declarative transaction management with annotations

Declarative transaction management with annotations

Module 5. Spring
Level 6 , Lesson 3
Available

Imagine you'd have to manually open and close transactions every time something goes wrong in your code. It's like sorting huge arrays by hand instead of using Java's built-in methods. Sure, you could — but why?

Declarative transaction management lets us ditch explicit transaction handling code (like beginTransaction and commit) and focus on the application logic. Spring takes care of the hassle — creating, managing, and rolling back transactions.

Benefits of the declarative approach

  1. Improved code readability: the application code gets cleaner because we don't have to explicitly write transaction handling logic.
  2. Fewer mistakes: the risk of forgetting to close a transaction is minimal since Spring handles it.
  3. More time for the important stuff: instead of wrestling with transactions you can focus on business logic (or pizza: your call).

Declarative transaction management in Spring

Now let's see how Spring provides declarative transaction management via annotations. Behind the scenes AOP (aspect-oriented programming) is at work. Spring creates proxy objects of your components and injects transaction management logic at method entry points. This approach is called "declarative" because we declare the transaction parameters and Spring does the rest.

How does a @Transactional transaction work?

  • When a method annotated with @Transactional is called, a proxy is created that manages the transaction.
  • The proxy intercepts the method call, opens a transaction, and ensures the transaction is rolled back in case of an exception.
  • If the method completes successfully, the transaction is committed.

Roughly, the interaction under the hood looks like this:


Method is called -> Proxy checks @Transactional -> Transaction opens ->
-> Method executes -> Success? commit() : rollback()

Configuring declarative management

1. Enabling transaction management

To enable transactions in your project you only need to do a couple of steps:

  1. Add a dependency on spring-tx to your pom.xml or build.gradle (if you're using Spring Boot, it's already included):
    
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
    </dependency>
    
  2. Enable support for the @Transactional annotation in your app. Add the @EnableTransactionManagement annotation to your configuration class:
    
    @Configuration
    @EnableTransactionManagement
    public class AppConfig {
        // You can configure your application's beans here
    }
    

2. Using @Transactional on methods

After setup you can annotate methods with @Transactional and let it manage transactions.

Here's an example:


@Service
public class OrderService {

    @Transactional
    public void createOrder(Order order) {
        // Step 1: Saving the order
        orderRepository.save(order);

        // Step 2: Updating the user's balance
        userAccountService.decreaseBalance(order.getUserId(), order.getTotalPrice());

        // Let's artificially create an error (e.g., division by zero)
        int error = 1 / 0;

        // Without @Transactional, changes would have been persisted to the database,
        // but the transaction will be automatically rolled back because of the exception.
    }
}

Here we intentionally break the code by dividing by zero. But thanks to the @Transactional annotation everything is rolled back! The user won't lose money and the database stays intact.


3. Flexibility of @Transactional settings

Annotation parameters: @Transactional allows you to specify several parameters:

  • propagation: how the transaction should behave when methods call each other.
  • isolation: transaction isolation level (e.g., READ_COMMITTED, SERIALIZABLE).
  • rollbackFor: exceptions that should trigger a rollback.
  • timeout: transaction timeout.
  • readOnly: set true if the transaction only reads data.

Example of configuring parameters:


@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.SERIALIZABLE,
    timeout = 5,
    rollbackFor = {SQLException.class, CustomException.class},
    readOnly = false
)
public void processOrder(Order order) {
    // Your code
}

Example with propagation

Spring supports several transaction behaviors. For example:

  • REQUIRED: use the existing transaction or create a new one.
  • REQUIRES_NEW: always create a new transaction.
  • NESTED: nested transactions.

Example of different behaviors:


@Service
public class PaymentService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void processPayment() {
        // Code for processing the payment
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logPayment() {
        // Code for logging the transaction
    }
}

How does Spring implement declarative management? (Under the hood)

Spring uses AOP and creates a proxy object. That means when you call a method annotated with @Transactional, the real call goes through the proxy, which adds transaction management. For example, the method gets wrapped in something like:


transactionManager.begin();
try {
    method(); // Call to the real method
    transactionManager.commit();
} catch (Exception e) {
    transactionManager.rollback();
}

This magic lets us avoid manual transaction management.


Practical example: transaction management in an e-commerce app

Let's build a simple e-commerce scenario where a user places an order. We'll use @Transactional to manage all operations: saving the order, updating the user's balance.


@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private UserAccountService userAccountService;

    @Transactional
    public void placeOrder(Order order) {
        // Save the order
        orderRepository.save(order);

        // Update the user's balance
        userAccountService.decreaseBalance(order.getUserId(), order.getTotalPrice());

        // Artificial error creation
        if (order.getTotalPrice() > 10000) {
            throw new RuntimeException("Budget exceeded!");
        }
    }
}

When the budget is exceeded the transaction rolls back. We end up with consistent data and a happy user.

Verifying rollback manually

You can write a test to verify that data is rolled back when an exception occurs:


@SpringBootTest
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    public void testTransactionRollback() {
        Order order = new Order(/* order data */);

        try {
            orderService.placeOrder(order);
        } catch (RuntimeException e) {
            // Catch RuntimeException
        }

        // Make sure the order is NOT saved in the database
        assertEquals(0, orderRepository.count());
    }
}

Typical mistakes and gotchas

  1. Is @Transactional not working?
    • Check that you've enabled @EnableTransactionManagement. Without it the annotation won't work.
    • Make sure the method with @Transactional is called via a Spring bean, otherwise the proxy won't be used.
  2. Transaction not rolling back?
    • By default Spring only rolls back for RuntimeException. If you want to handle other exceptions, use the rollbackFor parameter.
  3. Nested transactions can be confusing:
    • Remember that propagation = REQUIRES_NEW creates a new transaction, whereas REQUIRED continues the current one.

At this point you're ready to manage transactions with the @Transactional annotation. May your data stay consistent and your systems resilient!

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION