CodeGym /Courses /Module 5. Spring /The @Transactional Annotation: Key Features

The @Transactional Annotation: Key Features

Module 5. Spring
Level 6 , Lesson 2
Available

Imagine this scenario: you need to implement a money transfer between two bank accounts. Obviously, the operation consists of two steps:

  1. Debit funds from one account.
  2. Credit funds to the other account.

If something goes wrong during the process — for example, a failure on the second step — it's important to roll back the whole operation to avoid a situation where the money "disappears". This is where the @Transactional annotation comes into play, letting you focus on business logic while Spring handles transaction management, making the code cleaner and simpler.


How does @Transactional work?

@Transactional is an annotation that defines transaction behavior for a method or class. Spring uses aspect-oriented programming (AOP) to wrap your code in "transactional proxies". That means when a method runs, Spring automatically starts a transaction, and when the method finishes — it either commits the changes (commit) or rolls them back (rollback) if an exception occurred.

Here's an example of using @Transactional for a money transfer between accounts:


@Service
public class BankService {

    @Transactional
    public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        // Step 1: Debit funds from one account
        accountRepository.debit(fromAccountId, amount);

        // Step 2: Credit funds to the other account
        accountRepository.credit(toAccountId, amount);
    }
}

Simple? But that's just the tip of the iceberg.


Options on @Transactional

The @Transactional annotation provides a bunch of options to control transaction behavior. Let's go over the most important ones.

1. Propagation (how the transaction is propagated)

This specifies how a transaction should behave if the method is called inside another transaction. For example:

  • REQUIRED (default): Uses the existing transaction if there is one. If not, creates a new one.
  • REQUIRES_NEW: Always creates a new transaction, suspending the current one.
  • MANDATORY: Requires an existing transaction. If none exists — it will throw an exception.
  • SUPPORTS: Runs within a transaction if one exists, but can be called without one.

Example:


@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(AuditLog log) {
    auditLogRepository.save(log);
}

2. Isolation (isolation level)

Specifies how a transaction should see changes made by other transactions. This matters in multi-user systems.

Here are the main isolation levels:

  • READ_UNCOMMITTED: a transaction can see uncommitted changes from other transactions (dirty reads).
  • READ_COMMITTED: prevents dirty reads.
  • REPEATABLE_READ: protection against non-repeatable reads (consistent reads).
  • SERIALIZABLE: full isolation, maximum protection from conflicts, but with a performance hit.

Example:


@Transactional(isolation = Isolation.SERIALIZABLE)
public void performSensitiveOperation() {
    // Mission-critical operation
}

3. Timeout (max duration)

Sometimes transactions can get stuck or run too long. You can set a time limit (in seconds), after which the transaction will be aborted.

Example:


@Transactional(timeout = 5) // Max 5 seconds
public void executeLongQuery() {
    // Long operation
}

4. ReadOnly (read-only)

If the transaction only reads data (no changes), you can set readOnly=true. This can optimize performance.

Example:


@Transactional(readOnly = true)
public List<Account> getAllAccounts() {
    return accountRepository.findAll();
}
WARNING:
Even though this flag indicates the method shouldn't modify data, at the database level this doesn't always mean the queries will be faster.

5. RollbackFor (rollback on error)

By default Spring rolls back a transaction only for RuntimeException or Error. If you want to roll back on other exceptions (for example, Exception), you need to specify it explicitly:


@Transactional(rollbackFor = Exception.class)
public void processTransaction() throws Exception {
    // Code that might throw an exception
}

Real-world examples

Example 1: Order processing in an online store

Imagine processing an order that includes:

  • Decreasing product stock
  • Charging the customer's card
  • Recording the order data

@Service
public class OrderService {

    @Transactional
    public void processOrder(Order order) {
        // Check product availability
        stockService.decreaseStock(order.getProductId(), order.getQuantity());

        // Charge payment
        paymentService.charge(order.getPaymentInfo());

        // Save order
        orderRepository.save(order);
    }
}

If any step fails, all changes will be rolled back.

Example 2: Error logging

Sometimes you want to save an event log outside the main transaction. Use REQUIRES_NEW:


@Service
public class LogService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logError(String message) {
        errorLogRepository.save(new ErrorLog(message));
    }
}

Common mistakes when working with @Transactional

  1. The annotation didn't take effect. This can happen if the method is called from the same class (via this). Remember Spring uses proxies, and internal calls inside the same class bypass AOP. Solution? Move the method to another @Service.
  2. Mixing external and internal transactions. For example, using @Transactional(propagation = Propagation.REQUIRES_NEW) inside a method can introduce overhead. Use it only when necessary.
  3. Incorrect rollback behavior. If you forgot to specify rollbackFor, Spring might not roll back the transaction. Always explicitly declare the behavior you need.

Practice: Implementing @Transactional in an application

Let's create a method to process an order with automatic rollback on error:


@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private OrderRepository orderRepository;

    @Transactional
    public void placeOrder(Long productId, int quantity) {
        // Decrease product stock
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found!"));

        if (product.getStock() < quantity) {
            throw new RuntimeException("Not enough stock available!");
        }

        product.setStock(product.getStock() - quantity);
        productRepository.save(product);

        // Save the order
        Order order = new Order();
        order.setProductId(productId);
        order.setQuantity(quantity);
        orderRepository.save(order);

        // Artificial failure to test rollback
        if (quantity > 10) {
            throw new RuntimeException("Failure while processing the order!");
        }
    }
}

Check:

  1. Calling placeOrder with valid data — will create the order and reduce the product stock.
  2. Running with a large quantity (quantity > 10) — will trigger a failure. Spring will roll back the changes, keeping the data consistent.

@Transactional is a powerful tool that saves you the headache of manual transaction management. Use it responsibly, tune the parameters, and always validate behavior with tests!

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